Skip to content

Commit

Permalink
Refactoring.
Browse files Browse the repository at this point in the history
  • Loading branch information
interkosmos committed Oct 19, 2024
1 parent 834a908 commit 90ee7f5
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 133 deletions.
171 changes: 46 additions & 125 deletions src/dm_camera.f90
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ module dm_camera
!! Module for taking still images from RTSP video streams and USB webcams,
!! using FFmpeg.
!!
!! On Linux, install the packages `ffmpeg`, `graphicsmagick`, and
!! `v4l-utils`:
!! On Linux, install the packages `ffmpeg` and `v4l-utils`:
!!
!! ```
!! $ sudo apt-get install ffmpeg graphicsmagick v4l-utils
!! $ sudo apt-get install ffmpeg v4l-utils
!! ```
!!
!! List connected USB cameras:
Expand All @@ -21,31 +20,13 @@ module dm_camera
!! /dev/media0
!! ```
!!
!! GraphicsMagick is required to add text to captured camera frames. For a
!! list of all supported font names, run:
!!
!! ```
!! $ gm convert -list font
!! Path: /usr/local/lib/GraphicsMagick/config/type-windows.mgk
!!
!! Name Family Style Stretch Weight
!! --------------------------------------------------------------------------------
!! Arial Arial normal normal 400
!! Arial-Black Arial normal normal 900
!! Arial-Bold Arial normal normal 700
!! Arial-Bold-Italic Arial italic normal 700
!! Arial-Italic Arial italic normal 400
!! ...
!! ```
!!
!! The default font used is `Lucida-Console` in 12 points size.
!!
!! The following example captures an image from an attached USB webcam at
!! `/dev/video0` and adds a timestamp in ISO 8601 to it:
!! `/dev/video0` and adds a timestamp in ISO 8601 format to it:
!!
!! ```fortran
!! character(len=*), parameter :: IMAGE_PATH = '/tmp/image.jpg'
!!
!! integer :: width, height
!! integer :: rc
!! type(camera_type) :: camera
!!
Expand All @@ -54,9 +35,22 @@ module dm_camera
!! rc = dm_camera_capture(camera, IMAGE_PATH)
!! if (dm_is_error(rc)) call dm_error_out(rc)
!!
!! rc = dm_camera_image_add_text(IMAGE_PATH, text=dm_time_now())
!! rc = dm_image_add_text_box(IMAGE_PATH, text=dm_time_now())
!! if (dm_is_error(rc)) call dm_error_out(rc)
!!
!! rc = dm_image_get_dimensions(IMAGE_PATH, width, height)
!! if (dm_is_error(rc)) call dm_error_out(rc)
!!
!! print '("image dimensions: ", i0, "x", i0)', width, height
!! ```
!!
!! Change the camera type to capture an RTSP stream instead:
!!
!! ```fortran
!! camera = camera_type(input='rtsp://10.10.10.15:8554/camera1', device=CAMERA_DEVICE_RTSP)
!! ```
!!
!! The attribute `input` must be the URL of the stream.
use :: dm_error
use :: dm_file
use :: dm_string
Expand All @@ -70,13 +64,9 @@ module dm_camera
integer, parameter, public :: CAMERA_DEVICE_V4L = 2 !! USB webcam via Video4Linux2.
integer, parameter, public :: CAMERA_DEVICE_LAST = 2 !! Never use this.

integer, parameter, public :: CAMERA_COLOR_LEN = 32 !! Max. length of GM colour name.
integer, parameter, public :: CAMERA_COMMAND_LEN = FILE_PATH_LEN !! Max. length of command string.
integer, parameter, public :: CAMERA_FONT_LEN = 64 !! Max. length of GraphicsMagick font name.
integer, parameter, public :: CAMERA_GRAVITY_LEN = 32 !! Max. length of GM gravity.

character(len=*), parameter :: CAMERA_FFMPEG = 'ffmpeg' !! FFmpeg binary name.
character(len=*), parameter :: CAMERA_GM = 'gm' !! GraphicsMagick binary name.

type, public :: camera_type
!! Camera settings type.
Expand All @@ -86,25 +76,12 @@ module dm_camera
integer :: height = 0 !! Camera stream height in pixels (optional).
end type camera_type

type, public :: camera_text_box_type
!! Text box settings for drawing text onto camera frame image.
character(len=CAMERA_GRAVITY_LEN) :: gravity = 'SouthWest' !! Text position.
character(len=CAMERA_COLOR_LEN) :: background = 'black' !! Box colour.
character(len=CAMERA_COLOR_LEN) :: foreground = 'white' !! Text colour.
character(len=CAMERA_FONT_LEN) :: font = 'Lucida-Console' !! GraphicsMagick font name.
integer :: font_size = 12 !! Font size in points.
end type camera_text_box_type

public :: dm_camera_capture
public :: dm_camera_device_is_valid
public :: dm_camera_image_add_text
public :: dm_camera_prepare_command_ffmpeg
public :: dm_camera_prepare_command_gm

private :: camera_prepare_capture
contains
! **************************************************************************
! PUBLIC FUNCTIONS.
! **************************************************************************
integer function dm_camera_capture(camera, output) result(rc)
integer function dm_camera_capture(camera, output, command) result(rc)
!! Captures a single frame from a V4L device or RTSP stream with
!! FFmpeg, and optionally adds a timestamp with GraphicsMagick. If the
!! input is an RTSP stream, the URL must start with `rtsp://`.
Expand All @@ -115,27 +92,32 @@ integer function dm_camera_capture(camera, output) result(rc)
!! * `E_INVALID` if camera device or RTSP stream URL is invalid.
!! * `E_IO` if FFmpeg command execution failed.
!!
type(camera_type), intent(in) :: camera !! Camera type.
character(len=*), intent(in) :: output !! Output file.
type(camera_type), intent(in) :: camera !! Camera type.
character(len=*), intent(in) :: output !! Output file.
character(len=:), allocatable, intent(out), optional :: command !! Executed command.

character(len=CAMERA_COMMAND_LEN) :: command_
integer :: stat

character(len=CAMERA_COMMAND_LEN) :: command
integer :: exit_stat
command_ = ' '

rc = E_EMPTY
if (len_trim(camera%input) == 0 .or. len_trim(output) == 0) return
io_block: block
rc = E_EMPTY
if (len_trim(camera%input) == 0 .or. len_trim(output) == 0) exit io_block

rc = E_INVALID
if (.not. dm_camera_device_is_valid(camera%device)) return
if (camera%device == CAMERA_DEVICE_RTSP .and. .not. dm_string_starts_with(camera%input, 'rtsp://')) return
rc = E_INVALID
if (.not. dm_camera_device_is_valid(camera%device)) return
if (camera%device == CAMERA_DEVICE_RTSP .and. .not. dm_string_starts_with(camera%input, 'rtsp://')) exit io_block

rc = E_IO
call dm_camera_prepare_command_ffmpeg(command, camera, output)
call execute_command_line(trim(command), exitstat=exit_stat)
rc = E_IO
call camera_prepare_capture(command_, camera, output)
call execute_command_line(trim(command_), exitstat=stat)
if (stat /= 0 .or. .not. dm_file_exists(output)) exit io_block

if (exit_stat /= 0) return
if (.not. dm_file_exists(output)) return
rc = E_NONE
end block io_block

rc = E_NONE
if (present(command)) command = trim(command_)
end function dm_camera_capture

logical function dm_camera_device_is_valid(device) result(is)
Expand All @@ -146,41 +128,7 @@ logical function dm_camera_device_is_valid(device) result(is)
is = (device > CAMERA_DEVICE_NONE .and. device <= CAMERA_DEVICE_LAST)
end function dm_camera_device_is_valid

integer function dm_camera_image_add_text(path, text, box) result(rc)
!! Draws text onto camera image file, using GraphicsMagick. By default,
!! the text box is drawn to the bottom-left corner of the image.
!!
!! The function returns the following error codes:
!!
!! * `E_EMPTY` if text or image path are empty.
!! * `E_IO` if GraphicsMagick command execution failed.
!! * `E_NOT_FOUND` if image at given path does no exist.
!!
character(len=*), intent(in) :: path !! Image file path.
character(len=*), intent(in) :: text !! Text to add.
type(camera_text_box_type), intent(in), optional :: box !! Camera box type.

character(len=CAMERA_COMMAND_LEN) :: command
integer :: exit_stat

rc = E_EMPTY
if (len_trim(path) == 0 .or. len_trim(text) == 0) return

rc = E_NOT_FOUND
if (.not. dm_file_exists(path)) return

rc = E_IO
call dm_camera_prepare_command_gm(command, path, text, box)
call execute_command_line(trim(command), exitstat=exit_stat)
if (exit_stat /= 0) return

rc = E_NONE
end function dm_camera_image_add_text

! **************************************************************************
! PUBLIC SUBROUTINES.
! **************************************************************************
subroutine dm_camera_prepare_command_ffmpeg(command, camera, output)
subroutine camera_prepare_capture(command, camera, output)
!! Creates FFmpeg command to capture a single camera frame through V4L
!! or RTSP. The function returns `E_INVALID` on error.
character(len=CAMERA_COMMAND_LEN), intent(out) :: command !! Prepared command string.
Expand All @@ -192,12 +140,11 @@ subroutine dm_camera_prepare_command_ffmpeg(command, camera, output)
! Disable logging and set output file.
command = ' -hide_banner -loglevel fatal -nostats -y ' // output

! Format argument `-f` must be before input argument `-i`.
select case (camera%device)
case (CAMERA_DEVICE_RTSP)
! Capture RTSP stream for 0.5 seconds to get key frame,
! overwrite output file.
command = ' -f image2 -i ' // trim(camera%input) // ' -update 1 -t 0.5' // command
command = ' -i ' // trim(camera%input) // ' -f image2 -update 1 -t 0.5' // command

case (CAMERA_DEVICE_V4L)
! Capture single frame from V4L device.
Expand All @@ -206,37 +153,11 @@ subroutine dm_camera_prepare_command_ffmpeg(command, camera, output)
command = trim(video_size) // command
end if

! Format argument `-f` must be before input argument `-i`.
command = ' -f v4l2 -i ' // trim(camera%input) // ' -frames:v 1' // command
end select

! Concatenate command string.
command = CAMERA_FFMPEG // command
end subroutine dm_camera_prepare_command_ffmpeg

subroutine dm_camera_prepare_command_gm(command, path, text, box)
!! Prepares GraphicsMagick command to add text to image.
character(len=CAMERA_COMMAND_LEN), intent(out) :: command !! Prepared command string.
character(len=*), intent(in) :: path !! Image file path.
character(len=*), intent(in) :: text !! Text to add.
type(camera_text_box_type), intent(in), optional :: box !! Camera box type.

character(len=32) :: point_size
type(camera_text_box_type) :: box_

if (present(box)) box_ = box

write (command, '(" -gravity ", a, " -box ", a, " -fill ", a, " -draw ''text 0,0 """, a, """''", 2(1x, a))') &
trim(box_%gravity), trim(box_%background), trim(box_%foreground), trim(text), trim(path), path

if (box_%font_size > 0) then
write (point_size, '(" -pointsize ", i0)') box_%font_size
command = trim(point_size) // command
end if

if (len_trim(box_%font) > 0) then
command = ' -font ' // trim(box_%font) // command
end if

command = CAMERA_GM // ' convert' // command
end subroutine dm_camera_prepare_command_gm
command = CAMERA_FFMPEG // trim(command)
end subroutine camera_prepare_capture
end module dm_camera
Loading

0 comments on commit 90ee7f5

Please sign in to comment.