Skip to content

Commit

Permalink
Converted capturing-images.
Browse files Browse the repository at this point in the history
  • Loading branch information
eunoia-cl committed Sep 20, 2021
1 parent 7b3ef62 commit cf25028
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 422 deletions.
Binary file modified docs/ch11-multimedia/assets/camera-ui.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
125 changes: 83 additions & 42 deletions docs/ch11-multimedia/capturing-images.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
# Capturing Images

One of the key features of the `Camera` element is that is can be used to take pictures. We will use this in a simple stop-motion application. By building the application, you will learn how to show a viewfinder, snap photos and keep track of the pictures taken.

The user interface is shown below. It consists of three major parts. In the background, you will find the viewfinder, to the right, a column of buttons and at the bottom, a list of images taken. The idea is to take a series of photos, then click the Play Sequence button. This will play the images back, creating a simple stop-motion film.

One of the key features of the `Camera` element is that is can be used to take pictures. We will use this in a simple stop-motion application. By building the application, you will learn how to show a viewfinder, switch between cameras, snap photos and keep track of the pictures taken.

The user interface is shown below. It consists of three major parts. In the background, you will find the viewfinder, to the right, a column of buttons and at the bottom, a list of images taken. The idea is to take a series of photos, then click the `Play Sequence` button. This will play the images back, creating a simple stop-motion film.

![image](./assets/camera-ui.png)

The viewfinder part of the camera is simply a `Camera` element used as `source` in a `VideoOutput`. This will show the user a live video stream from the camera.
## The viewfinder

The viewfinder part of the camera is made using a `VideoOutput` element as video output channel of a `CaptureSession`. The `CaptureSession` in turns uses a `Camera` component to configure the device. This will display a live video stream from the camera.

```qml
VideoOutput {
anchors.fill: parent
source: camera
CaptureSession {
id: captureSession
videoOutput: output
camera: Camera {}
imageCapture: ImageCapture {
/* ... */
}
}
Camera {
id: camera
VideoOutput {
id: output
anchors.fill: parent
}
```

::: tip
For more control over the camera behaviour, for instance to control exposure or focus settings, use the `exposure` and `focus` properties of the `Camera` object. These are enums providing detailed control of the camear.
You can have more control on the camera behaviour by using dedicated `Camera` properties such as `exposureMode`, `whiteBalanceMode` or `zoomFactor`.
:::

The list of photos is a `ListView` oriented horizontally shows images from a `ListModel` called `imagePaths`. In the background, a semi-transparent black `Rectangle` is used.
## The captured images list

The list of photos is a `ListView` oriented horizontally that shows images from a `ListModel` called `imagePaths`. In the background, a semi-transparent black `Rectangle` is used.

```qml
ListModel {
Expand Down Expand Up @@ -63,54 +70,91 @@ ListView {
}
```

For the shooting of images, you need to know that the `Camera` element contains a set of sub-elements for various tasks. To capture still pictures, the `Camera.imageCapture` element is used. When you call the `capture` method, a picture is taken. This results in the `Camera.imageCapture` emitting first the `imageCaptured` signal followed by the `imageSaved` signal.
For the shooting of images, the `CaptureSession` element contains a set of sub-elements for various tasks. To capture still pictures, the `CaptureSession.imageCapture` element is used. When you call the `captureToFile` method, a picture is taken and saved in the user's local pictures directory. This results in the `CaptureSession.imageCapture` emitting the `imageSaved` signal.

```qml
Button {
id: shotButton
width: parent.buttonWidth
height: parent.buttonHeight
text: "Take Photo"
onClicked: {
camera.imageCapture.capture();
captureSession.imageCapture.captureToFile()
}
}
```

In this case, we don’t need to show a preview image, but simply add the resulting image to the `ListView` at the bottom of the screen. Shown in the example below, the path to the saved image is provided as the `path` argument with the signal.

```qml
CaptureSession {
/* ... */
imageCapture: ImageCapture {
onImageSaved: function (id, path) {
imagePaths.append({"path": path})
listView.positionViewAtEnd()
}
}
}
```

To intercept the signals of a sub-element, a `Connections` element is needed. In this case, we don’t need to show a preview image, but simply add the resulting image to the `ListView` at the bottom of the screen. Shown in the example below, the path to the saved image is provided as the `path` argument with the signal.
:::tip
For showing a preview, connect to the `imageCaptured` signal and use the `preview` signal argument as `source` of an `Image` element. An `id` signal argument is sent along both the `imageCaptured` and `imageSaved`. This value is returned from the `capture` method. Using this, the capture of an image can be traced through the complete cycle. This way, the preview can be used first and then be replaced by the properly saved image. This, however, is nothing that we do in the example.
:::

## Switching between cameras

If the user has multiple cameras, it can be handy to provide a way of switching between those. It's possible to achieve this by using the `MediaDevices` element in conjunction with a `ListView`. In our case, we'll use a `ComboBox` component:

```qml
Connections {
target: camera.imageCapture
MediaDevices {
id: mediaDevices
}
ComboBox {
id: cameraComboBox
onImageSaved: {
imagePaths.append({"path": path})
listView.positionViewAtEnd();
width: parent.buttonWidth
height: parent.buttonHeight
model: mediaDevices.videoInputs
textRole: "description"
displayText: captureSession.camera.cameraDevice.description
onActivated: function (index) {
captureSession.camera.cameraDevice = cameraComboBox.currentValue
}
}
```

For showing a preview, connect to the `imageCaptured` signal and use the `preview` signal argument as `source` of an `Image` element. A `requestId` signal argument is sent along both the `imageCaptured` and `imageSaved`. This value is returned from the `capture` method. Using this, the capture of an image can be traced through the complete cycle. This way, the preview can be used first and then be replaced by the properly saved image. This, however, is nothing that we do in the example.
The `model` property of the `ComboBox` is set to the `videoInputs` property of our `MediaDevices`. This last property contains the list of usable video inputs. We then set the `displayText` of the control to the description of the camera device (`captureSession.camera.cameraDevice.description`).

Finally, when the user switches the video input, the cameraDevice is updated to reflect that change: `captureSession.camera.cameraDevice = cameraComboBox.currentValue`.

## The playback

The last part of the application is the actual playback. This is driven using a `Timer` element and some JavaScript. The `_imageIndex` variable is used to keep track of the currently shown image. When the last image has been shown, the playback is stopped. In the example, the `root.state` is used to hide parts of the user interface when playing the sequence.

```qml
property int _imageIndex: -1
function startPlayback()
{
root.state = "playing";
setImageIndex(0);
playTimer.start();
function startPlayback() {
root.state = "playing"
setImageIndex(0)
playTimer.start()
}
function setImageIndex(i)
{
_imageIndex = i;
function setImageIndex(i) {
_imageIndex = i
if (_imageIndex >= 0 && _imageIndex < imagePaths.count)
image.source = imagePaths.get(_imageIndex).path;
else
image.source = "";
if (_imageIndex >= 0 && _imageIndex < imagePaths.count) {
image.source = imagePaths.get(_imageIndex).path
} else {
image.source = ""
}
}
Timer {
Expand All @@ -120,15 +164,12 @@ Timer {
repeat: false
onTriggered: {
if (_imageIndex + 1 < imagePaths.count)
{
setImageIndex(_imageIndex + 1);
playTimer.start();
}
else
{
setImageIndex(-1);
root.state = "";
if (_imageIndex + 1 < imagePaths.count) {
setImageIndex(_imageIndex + 1)
playTimer.start()
} else {
setImageIndex(-1)
root.state = ""
}
}
}
Expand Down
201 changes: 201 additions & 0 deletions docs/ch11-multimedia/src/camera-capture/main.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import QtQuick
import QtQuick.Controls
import QtMultimedia

Rectangle {
id: root

width: 1024
height: 600

color: "black"

CaptureSession {
id: captureSession
videoOutput: output
camera: Camera {}
imageCapture: ImageCapture {
onImageSaved: function (id, path) {
imagePaths.append({"path": path})
listView.positionViewAtEnd()
}
}
}

MediaDevices {
id: mediaDevices
}

ListModel {
id: imagePaths
}

VideoOutput {
id: output
anchors.fill: parent
}

Image {
id: image
anchors.fill: parent
}

Window {
width: 200
height: 200
visible: true
Image {
id: testImage
anchors.fill: parent
source: captureSession.imageCapture.preview
}
}

ListView {
id: listView

anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: 10

height: 100

orientation: ListView.Horizontal
spacing: 10

model: imagePaths

delegate: Image {
height: 100
source: path
fillMode: Image.PreserveAspectFit
}

Rectangle {
anchors.fill: parent
anchors.topMargin: -10

color: "black"
opacity: 0.5
}
}

Column {
id: controls

property int buttonWidth: 170
property int buttonHeight: 50

anchors.top: parent.top
anchors.right: parent.right
anchors.margins: 10

spacing: 0

ComboBox {
id: cameraComboBox

width: parent.buttonWidth
height: parent.buttonHeight

model: mediaDevices.videoInputs
textRole: "description"

displayText: captureSession.camera.cameraDevice.description

onActivated: function (index) {
captureSession.camera.cameraDevice = cameraComboBox.currentValue
}
}

Button {
id: shotButton

width: parent.buttonWidth
height: parent.buttonHeight

text: "Take Photo"
onClicked: {
captureSession.imageCapture.captureToFile()
}
}

Button {
id: playButton

width: parent.buttonWidth
height: parent.buttonHeight

text: "Play Sequence"
onClicked: {
startPlayback()
}
}

Button {
id: clearButton

width: parent.buttonWidth
height: parent.buttonHeight

text: "Clear Sequence"
onClicked: {
imagePaths.clear()
}
}
}

property int _imageIndex: -1

function startPlayback() {
root.state = "playing"
setImageIndex(0)
playTimer.start()
}

function setImageIndex(i) {
_imageIndex = i

if (_imageIndex >= 0 && _imageIndex < imagePaths.count) {
image.source = imagePaths.get(_imageIndex).path
} else {
image.source = ""
}
}

Timer {
id: playTimer

interval: 200
repeat: false

onTriggered: {
if (_imageIndex + 1 < imagePaths.count) {
setImageIndex(_imageIndex + 1)
playTimer.start()
} else {
setImageIndex(-1)
root.state = ""
}
}
}

states: [
State {
name: "playing"
PropertyChanges { target: buttons; opacity: 0 }
PropertyChanges { target: listView; opacity: 0 }
}
]

transitions: [
Transition {
PropertyAnimation { properties: "opacity"; duration: 200 }
}
]

Component.onCompleted: {
captureSession.camera.start()
}
}
Loading

0 comments on commit cf25028

Please sign in to comment.