1#!/usr/bin/env python3

2#

3# This file is part of Checkbox.

4#

5# Copyright 2008-2023 Canonical Ltd.

6# Written by:

7# Fernando Bravo <fernando.bravo.hernandez@canonical.com>

8#

9# Checkbox is free software: you can redistribute it and/or modify

10# it under the terms of the GNU General Public License version 3,

11# as published by the Free Software Foundation.

12#

13# Checkbox is distributed in the hope that it will be useful,

14# but WITHOUT ANY WARRANTY; without even the implied warranty of

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

16# GNU General Public License for more details.

17#

18# You should have received a copy of the GNU General Public License

19# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.

20#

21

22 import argparse

23 from collections import deque

24 import logging

25 import os

26 import time

27 import sys

28

29 import cv2

30 import numpy as np

31

32 from checkbox_support.vendor.brisque.brisque import BRISQUE

33 from checkbox_support.helpers.timeout import timeout

34

35

36 THRESHOLD = 60

37 TIMEOUT = 10

38 MIN_INTERVAL = 0.5

39

40 logger = logging.getLogger("camera_quality_test")

41

42

43 def save_image(image: np.ndarray, device: str, output: str):

44 """Saves the image from a video device to a the output directory.

45

46 Args:

47 image (np.ndarray): image to save

48 device (str): name of the video device

49 output (str): output directory

50 """

51 filepath = os.path.join(output, "quality_image_{}.jpg".format(device))

52 # Check if the output directory exists

53 if not os.path.exists(output):

54 msg = "Output directory does not exist: {}".format(output)

55 raise RuntimeError(msg)

56 if not cv2.imwrite(filepath, image):

57 msg = "Error while saving the image"

58 raise RuntimeError(msg)

59 print("Saved image to {}".format(filepath))

60

61

62 def get_score_from_device(device: str, output: str = "") -> float:

63 """

64 "This function calculates the BRISQUE score for images captured by a

65 specified device within a given time window. If the score stabilizes

66 during this period, the function returns this stable value. If the score

67 does not stabilize within the time window, the function will return the

68 last computed score."

69

70 :param device:

71 The device to use for the webcam

72 :return:

73 The BRISQUE score for the image

74 :raises RuntimeError:

75 If the device cannot be opened or read

76 """

77 brisque = BRISQUE()

78

79 # Set the video device

80 index = int(device.replace("video", ""))

81 cam = cv2.VideoCapture(index)

82

83 if not cam.isOpened():

84 msg = "Cannot open the selected device: {}".format(device)

85 raise RuntimeError(msg)

86

87 # Compute the score for some time and check if it stabilizes

88 scores = deque(maxlen=4)

89 tmax = time.time() + TIMEOUT

90 iter_count = 0

91

92 # We compute the score for at most TIMEOUT seconds. If the computation is

93 # too slow, we iterate at least two times.

94 while time.time() < tmax or iter_count < 2:

95 # Compute the time for each iteration

96 start_compute = time.time()

97

98 # Compute the score

99 result, image = cam.read()

100 if not result:

101 msg = "Cannot read from the selected device: {}".format(device)

102 raise RuntimeError(msg)

103 score = brisque.score(image)

104

105 compute_time = time.time() - start_compute

106

107 # If the computation time is smaller than the minimum interval for

108 # this iteration, we wait for the remaining time to have enough

109 # time between iterations.

110 if compute_time < MIN_INTERVAL:

111 time.sleep(MIN_INTERVAL - compute_time)

112

113 # If the deviation of the scores is low enough for the last 4

114 # iterations, we can stop.

115 if len(scores) == 4 and np.std(scores) < 0.5:

116 break

117

118 scores.append(score)

119 iter_count += 1

120

121 if output:

122 save_image(image, device, output)

123

124 # Release the video device

125 cam.release()

126 return score

127

128

129 def evaluate_score(score: float) -> int:

130 """

131 Evaluate the BRISQUE score for an image and checks if it is below the

132 threshold value.

133

134 :param score:

135 The BRISQUE score to be evaluated

136 :returns:

137 0 if the test passes, 1 otherwise

138 """

139

140 if np.isnan(score):

141 msg = "Unable to compute BRISQUE score"

142 logger.error(msg)

143 return 1

144

145 elif score > THRESHOLD:

146 msg = "The BRISQUE score is too high: {} > {}".format(score, THRESHOLD)

147 logger.error(msg)

148 return 1

149

150 print("BRISQUE score: {}".format(score))

151 return 0

152

153

154@timeout(120)

155 def main(argv: list) -> int:

156 parser = argparse.ArgumentParser(description="Run the image quality test")

157 parser.add_argument(

158 "-d", "--device", default="video0", help="Device for the webcam to use"

159 )

160 parser.add_argument(

161 "-f", "--file", default="", help="Parse a file instead of a device"

162 )

163 parser.add_argument("-o", "--output", default="", help="Output directory")

164

165 args = parser.parse_args(argv)

166

167 if args.file:

168 img = cv2.imread(args.file)

169 brisque = BRISQUE()

170 score = brisque.score(img)

171 else:

172 score = get_score_from_device(args.device, args.output)

173

174 return evaluate_score(score)

175

176

177 if __name__ == "__main__":

178 sys.exit(main(sys.argv[1:]))