diff --git a/camera/camera.go b/camera/camera.go index 2d08b04..6dfaac9 100644 --- a/camera/camera.go +++ b/camera/camera.go @@ -1,5 +1,11 @@ package camera +import ( + "bytes" + "fmt" + "image" +) + // Options . type Options struct { Index int @@ -10,3 +16,84 @@ type Options struct { Timestamp bool TimeFormat string } + +var ( + yuy2FourCC = fourcc("YUY2") + yuyvFourCC = fourcc("YUYV") + mjpgFourCC = fourcc("MJPG") +) + +func fourcc(b string) uint32 { + return uint32(b[0]) | (uint32(b[1]) << 8) | (uint32(b[2]) << 16) | (uint32(b[3]) << 24) +} + +func bmp24ToRgba(data []byte, dst *image.RGBA) error { + r := bytes.NewReader(data) + + width := dst.Bounds().Dx() + height := dst.Bounds().Dy() + + // There are 3 bytes per pixel, and each row is 4-byte aligned. + b := make([]byte, (3*width+3)&^3) + + // BMP images are stored bottom-up rather than top-down. + for y := height - 1; y >= 0; y-- { + _, err := r.Read(b) + if err != nil { + return err + } + + p := dst.Pix[y*dst.Stride : y*dst.Stride+width*4] + for i, j := 0, 0; i < len(p); i, j = i+4, j+3 { + // BMP images are stored in BGR order rather than RGB order. + p[i+0] = b[j+2] + p[i+1] = b[j+1] + p[i+2] = b[j+0] + p[i+3] = 0xFF + } + } + + return nil +} + +// yuy2ToYCbCr422 converts a YUY2 (YUYV) byte slice to an image.YCbCr with YCbCrSubsampleRatio422 (I422). +func yuy2ToYCbCr422(data []byte, dst *image.YCbCr) error { + if dst.SubsampleRatio != image.YCbCrSubsampleRatio422 { + return fmt.Errorf("subsample ratio must be 422, got %s", dst.SubsampleRatio.String()) + } + + width := dst.Bounds().Dx() + height := dst.Bounds().Dy() + + if width%2 != 0 { + return fmt.Errorf("width must be even for YUY2") + } + + if len(data) != width*height*2 { + return fmt.Errorf("invalid data length for YUY2") + } + + stride := width * 2 // 2 bytes per pixel + + for y := 0; y < height; y++ { + for x := 0; x < width; x += 2 { + idx := y*stride + x*2 + + y0 := data[idx+0] + cb := data[idx+1] + y1 := data[idx+2] + cr := data[idx+3] + + // Y plane: every pixel + dst.Y[y*dst.YStride+x+0] = y0 + dst.Y[y*dst.YStride+x+1] = y1 + + // Cb/Cr plane: every 2 pixels (422) + off := y*dst.CStride + x/2 + dst.Cb[off] = cb + dst.Cr[off] = cr + } + } + + return nil +} diff --git a/camera/camera_linux.go b/camera/camera_linux.go index a491f5c..6510a69 100644 --- a/camera/camera_linux.go +++ b/camera/camera_linux.go @@ -6,9 +6,10 @@ package camera import ( "fmt" "image" + "io" + "slices" "github.com/korandiz/v4l" - "github.com/korandiz/v4l/fmt/mjpeg" im "github.com/gen2brain/cam2ip/image" ) @@ -17,12 +18,14 @@ import ( type Camera struct { opts Options camera *v4l.Device + config v4l.DeviceConfig + ycbcr *image.YCbCr } // New returns new Camera for given camera index. -func New(opts Options) (camera *Camera, err error) { - camera = &Camera{} - camera.opts = opts +func New(opts Options) (c *Camera, err error) { + c = &Camera{} + c.opts = opts devices := v4l.FindDevices() if len(devices) < opts.Index+1 { @@ -31,40 +34,65 @@ func New(opts Options) (camera *Camera, err error) { return } - camera.camera, err = v4l.Open(devices[opts.Index].Path) + c.camera, err = v4l.Open(devices[opts.Index].Path) if err != nil { err = fmt.Errorf("camera: %w", err) return } - if camera.camera == nil { + if c.camera == nil { err = fmt.Errorf("camera: can not open camera %d", opts.Index) return } - config, err := camera.camera.GetConfig() - if err != nil { - err = fmt.Errorf("camera: %w", err) + configs, e := c.camera.ListConfigs() + if e != nil { + err = fmt.Errorf("camera: can not list configs: %w", e) return } - config.Format = mjpeg.FourCC - config.Width = int(opts.Width) - config.Height = int(opts.Height) + formats := make([]uint32, 0) + for _, config := range configs { + formats = append(formats, config.Format) + } - err = camera.camera.SetConfig(config) + c.config, err = c.camera.GetConfig() if err != nil { - err = fmt.Errorf("camera: %w", err) + err = fmt.Errorf("camera: can not get config: %w", err) return } - err = camera.camera.TurnOn() + if slices.Contains(formats, mjpgFourCC) { + c.config.Format = mjpgFourCC + } else if slices.Contains(formats, yuyvFourCC) { + c.config.Format = yuyvFourCC + } else { + err = fmt.Errorf("camera: unsupported format %d", c.config.Format) + + return + } + + c.config.Width = int(opts.Width) + c.config.Height = int(opts.Height) + + err = c.camera.SetConfig(c.config) if err != nil { - err = fmt.Errorf("camera: %w", err) + err = fmt.Errorf("camera: format %d: can not set config: %w", c.config.Format, err) + + return + } + + if c.config.Format == yuyvFourCC { + c.ycbcr = image.NewYCbCr(image.Rect(0, 0, int(c.opts.Width), int(c.opts.Height)), image.YCbCrSubsampleRatio422) + } + + err = c.camera.TurnOn() + if err != nil { + err = fmt.Errorf("camera: format %d: can not turn on: %w", c.config.Format, err) return } @@ -76,16 +104,35 @@ func New(opts Options) (camera *Camera, err error) { func (c *Camera) Read() (img image.Image, err error) { buffer, err := c.camera.Capture() if err != nil { - err = fmt.Errorf("camera: can not grab frame: %w", err) + err = fmt.Errorf("camera: format %d: can not grab frame: %w", c.config.Format, err) return } - img, err = im.NewDecoder(buffer).Decode() - if err != nil { - err = fmt.Errorf("camera: %w", err) + switch c.config.Format { + case yuy2FourCC, yuyvFourCC: + data, e := io.ReadAll(buffer) + if e != nil { + err = fmt.Errorf("camera: format %d: can not read buffer: %w", c.config.Format, e) - return + return + } + + e = yuy2ToYCbCr422(data, c.ycbcr) + if e != nil { + err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.config.Format, e) + + return + } + + img = c.ycbcr + case mjpgFourCC: + img, err = im.NewDecoder(buffer).Decode() + if err != nil { + err = fmt.Errorf("camera: format %d: can not decode frame: %w", c.config.Format, err) + + return + } } if c.opts.Rotate != 0 { @@ -97,7 +144,7 @@ func (c *Camera) Read() (img image.Image, err error) { } if c.opts.Timestamp { - img, err = im.Timestamp(img, "") + img, err = im.Timestamp(img, c.opts.TimeFormat) } return @@ -106,7 +153,7 @@ func (c *Camera) Read() (img image.Image, err error) { // Close closes camera. func (c *Camera) Close() (err error) { if c.camera == nil { - err = fmt.Errorf("camera: camera is not opened") + err = fmt.Errorf("camera: close: camera is not opened") return } diff --git a/camera/camera_opencv.go b/camera/camera_opencv.go index 1c2a730..952e122 100644 --- a/camera/camera_opencv.go +++ b/camera/camera_opencv.go @@ -74,7 +74,7 @@ func (c *Camera) Read() (img image.Image, err error) { } if c.opts.Timestamp { - img, err = im.Timestamp(img, "") + img, err = im.Timestamp(img, c.opts.TimeFormat) } return diff --git a/camera/camera_test.go b/camera/camera_test.go index ba2b04c..ccbb129 100644 --- a/camera/camera_test.go +++ b/camera/camera_test.go @@ -10,7 +10,7 @@ import ( ) func TestCamera(t *testing.T) { - camera, err := New(Options{0, 0, 640, 480, false}) + camera, err := New(Options{0, 0, "", 640, 480, false, ""}) if err != nil { t.Fatal(err) } @@ -40,7 +40,7 @@ func TestCamera(t *testing.T) { t.Error(err) } - err = image.NewEncoder(io.Discard).Encode(img) + err = image.NewEncoder(io.Discard, 75).Encode(img) if err != nil { t.Error(err) } diff --git a/camera/camera_windows.go b/camera/camera_windows.go index 1120849..fc5177b 100644 --- a/camera/camera_windows.go +++ b/camera/camera_windows.go @@ -22,10 +22,12 @@ func init() { type Camera struct { opts Options camera syscall.Handle - frame *image.RGBA + rgba *image.RGBA + ycbcr *image.YCbCr hdr *videoHdr instance syscall.Handle className string + format uint32 } // New returns new Camera for given camera index. @@ -34,8 +36,6 @@ func New(opts Options) (camera *Camera, err error) { camera.opts = opts camera.className = "capWindowClass" - camera.frame = image.NewRGBA(image.Rect(0, 0, int(camera.opts.Width), int(camera.opts.Height))) - go func(c *Camera) { fn := func(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr { switch msg { @@ -45,6 +45,7 @@ func New(opts Options) (camera *Camera, err error) { postQuitMessage(0) default: ret := defWindowProc(hwnd, msg, wparam, lparam) + return ret } @@ -61,9 +62,11 @@ func New(opts Options) (camera *Camera, err error) { return } - hwnd, err := createWindow(0, c.className, "", wsOverlappedWindow, cwUseDefault, cwUseDefault, + hwnd, e := createWindow(0, c.className, "", wsOverlappedWindow, cwUseDefault, cwUseDefault, int64(c.opts.Width)+100, int64(c.opts.Height)+100, 0, 0, c.instance) - if err != nil { + if e != nil { + err = e + return } @@ -75,9 +78,13 @@ func New(opts Options) (camera *Camera, err error) { ret := sendMessage(c.camera, wmCapDriverConnect, uintptr(c.opts.Index), 0) if int(ret) == 0 { err = fmt.Errorf("camera: can not open camera %d", c.opts.Index) + return } + sendMessage(c.camera, wmCapSetPreview, 0, 0) + sendMessage(c.camera, wmCapSetOverlay, 0, 0) + var bi bitmapInfo size := sendMessage(c.camera, wmCapGetVideoformat, 0, 0) sendMessage(c.camera, wmCapGetVideoformat, size, uintptr(unsafe.Pointer(&bi))) @@ -87,17 +94,36 @@ func New(opts Options) (camera *Camera, err error) { ret = sendMessage(c.camera, wmCapSetVideoformat, size, uintptr(unsafe.Pointer(&bi))) if int(ret) == 0 { - err = fmt.Errorf("camera: can not set video format") + err = fmt.Errorf("camera: can not set video format: %dx%d, %d", int(c.opts.Width), int(c.opts.Height), c.format) + return } + c.format = bi.BmiHeader.BiCompression sendMessage(c.camera, wmCapSetCallbackFrame, 0, syscall.NewCallback(c.callback)) + switch c.format { + case 0: + if bi.BmiHeader.BiBitCount != 24 { + err = fmt.Errorf("camera: unsupported format %d; bitcount: %d", c.format, bi.BmiHeader.BiBitCount) + + return + } + + c.rgba = image.NewRGBA(image.Rect(0, 0, int(c.opts.Width), int(c.opts.Height))) + case yuy2FourCC, yuyvFourCC: + c.ycbcr = image.NewYCbCr(image.Rect(0, 0, int(c.opts.Width), int(c.opts.Height)), image.YCbCrSubsampleRatio422) + case mjpgFourCC: + default: + err = fmt.Errorf("camera: unsupported format %d", c.format) + + return + } + for { var msg msgW - ok, _ := getMessage(&msg, hwnd, 0, 0) + ok, _ := getMessage(&msg, 0, 0, 0) if ok { - //translateMessage(&msg) dispatchMessage(&msg) } else { break @@ -112,40 +138,44 @@ func New(opts Options) (camera *Camera, err error) { // Read reads next frame from camera and returns image. func (c *Camera) Read() (img image.Image, err error) { - ret := sendMessage(c.camera, wmCapGrabFrameNoStop, 0, 0) + ret := sendMessage(c.camera, wmCapGrabFrame, 0, 0) if int(ret) == 0 { err = fmt.Errorf("camera: can not grab frame") + return } - data := (*[1 << 24]uint8)(unsafe.Pointer(c.hdr.LpData))[0:c.hdr.DwBytesUsed] - r := bytes.NewReader(data) + data := unsafe.Slice((*byte)(unsafe.Pointer(c.hdr.LpData)), c.hdr.DwBufferLength) - width := int(c.opts.Width) - height := int(c.opts.Height) + switch c.format { + case 0: + e := bmp24ToRgba(data, c.rgba) + if e != nil { + err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.format, e) - // Taken from https://github.com/hotei/bmp/blob/master/bmpRGBA.go#L12 - // There are 3 bytes per pixel, and each row is 4-byte aligned. - b := make([]byte, (3*width+3)&^3) - // BMP images are stored bottom-up rather than top-down. - for y := height - 1; y >= 0; y-- { - _, err = r.Read(b) - if err != nil { - err = fmt.Errorf("camera: can not retrieve frame: %w", err) return } - p := c.frame.Pix[y*c.frame.Stride : y*c.frame.Stride+width*4] - for i, j := 0, 0; i < len(p); i, j = i+4, j+3 { - // BMP images are stored in BGR order rather than RGB order. - p[i+0] = b[j+2] - p[i+1] = b[j+1] - p[i+2] = b[j+0] - p[i+3] = 0xFF - } - } + img = c.rgba + case yuy2FourCC, yuyvFourCC: + e := yuy2ToYCbCr422(data, c.ycbcr) + if e != nil { + err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.format, e) - img = c.frame + return + } + + img = c.ycbcr + case mjpgFourCC: + i, e := im.NewDecoder(bytes.NewReader(data)).Decode() + if e != nil { + err = fmt.Errorf("camera: format %d: can not retrieve frame: %w", c.format, e) + + return + } + + img = i + } if c.opts.Rotate != 0 { img = im.Rotate(img, c.opts.Rotate) @@ -173,7 +203,9 @@ func (c *Camera) Close() (err error) { // callback function. func (c *Camera) callback(hwnd syscall.Handle, hdr *videoHdr) uintptr { - c.hdr = hdr + if hdr != nil { + c.hdr = hdr + } return 0 } @@ -183,16 +215,15 @@ var ( kernel32 = syscall.NewLazyDLL("kernel32.dll") avicap32 = syscall.NewLazyDLL("avicap32.dll") - createWindowExW = user32.NewProc("CreateWindowExW") - destroyWindowW = user32.NewProc("DestroyWindow") - defWindowProcW = user32.NewProc("DefWindowProcW") - dispatchMessageW = user32.NewProc("DispatchMessageW") - translateMessageW = user32.NewProc("TranslateMessage") - getMessageW = user32.NewProc("GetMessageW") - sendMessageW = user32.NewProc("SendMessageW") - postQuitMessageW = user32.NewProc("PostQuitMessage") - registerClassExW = user32.NewProc("RegisterClassExW") - unregisterClassW = user32.NewProc("UnregisterClassW") + createWindowExW = user32.NewProc("CreateWindowExW") + destroyWindowW = user32.NewProc("DestroyWindow") + defWindowProcW = user32.NewProc("DefWindowProcW") + dispatchMessageW = user32.NewProc("DispatchMessageW") + getMessageW = user32.NewProc("GetMessageW") + sendMessageW = user32.NewProc("SendMessageW") + postQuitMessageW = user32.NewProc("PostQuitMessage") + registerClassExW = user32.NewProc("RegisterClassExW") + unregisterClassW = user32.NewProc("UnregisterClassW") getModuleHandleW = kernel32.NewProc("GetModuleHandleW") capCreateCaptureWindowW = avicap32.NewProc("capCreateCaptureWindowW") @@ -209,6 +240,8 @@ const ( wmCapDriverDisconnect = wmCapStart + 11 wmCapGetVideoformat = wmCapStart + 44 wmCapSetVideoformat = wmCapStart + 45 + wmCapSetPreview = wmCapStart + 50 + wmCapSetOverlay = wmCapStart + 51 wmCapGrabFrame = wmCapStart + 60 wmCapGrabFrameNoStop = wmCapStart + 61 wmCapStop = wmCapStart + 68 @@ -337,11 +370,6 @@ func dispatchMessage(msg *msgW) { dispatchMessageW.Call(uintptr(unsafe.Pointer(msg))) } -// https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-translatemessage -func translateMessage(msg *msgW) { - translateMessageW.Call(uintptr(unsafe.Pointer(msg))) -} - // https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getmessagew func getMessage(msg *msgW, hwnd syscall.Handle, msgFilterMin, msgFilterMax uint32) (bool, error) { ret, _, err := getMessageW.Call(uintptr(unsafe.Pointer(msg)), uintptr(hwnd), uintptr(msgFilterMin), uintptr(msgFilterMax))