blob: fa55d042760f6bce8f492e9bbd8f32f84682965e [file] [log] [blame]
# Copyright 2012 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Fixtureless camera test.
Description
-----------
This pytest test if camera is working by one of the following method (choose
by argument ``mode``):
* ``'camera_assemble'``: Detect whether the camera is well assembled.
* ``'qr'``: Scan QR code of given string.
* ``'camera_assemble_qr'``: Run camera_assemble and qr mode together.
* ``'face'``: Recognize a human face.
* ``'timeout'``: Run camera capture until timeout.
* ``'frame_count'``: Run camera capture for specified frames.
* ``'manual'``: Show captured image.
* ``'manual_led'``: Light or blink camera LED.
* ``'brightness'``: Check the maximum brightness of frames.
Test Procedure
--------------
If ``e2e_mode`` is ``True``, the operator may be prompt to click on the 'Allow'
button on Chrome notification to give Chrome camera permission. Set
`--use-fake-ui-for-media-stream` in `/etc/chrome_dev.conf` to accept the
permission automatically.
The test procedure differs for each different modes:
* ``'camera_assemble'``: Operator prepares a white paper that is large enough
to cover the FOV of the camera. Test would pass automatically after
``num_frames_to_pass`` frames with white paper are captured.
* ``'qr'``: Operator put a QR code with content specified by ``QR_string``.
Test would pass automatically after ``num_frames_to_pass`` frames with QR code
are captured.
* ``'camera_assemble_qr'``: Operator prepares a white paper that has QR code on
it. The white paper should be large enough to cover the FOV of the camera,
and the QR code should locate at the specified detection region. Test would
pass automatically after ``num_frames_to_pass`` frames with white paper and
QR code are captured.
* ``'face'``: Operator show a face to the camera. Test would pass automatically
after ``num_frames_to_pass`` frames with detected face are captured.
* ``'timeout'``: No user interaction is required, the test pass after
``timeout_secs`` seconds.
* ``'frame_count'``: No user interaction is required, the test pass after
``num_frames_to_pass`` frames are captured.
* ``'manual'``: Screen would show the image captured by camera, and operator
judge whether the image looks good. Note that this methods require judgement
by operator, so may yield false positivity.
* ``'manual_led'``: The LED light of camera would either be constant on or
blinking, and operator need to press the correct key to pass the test.
* ``'brightness'``: No user interaction is required, the test pass after
``num_frames_to_pass`` frames are captured. Only the frames which the maximum
brightness is between `brightness_range` are counted.
Except ``'timeout'`` mode, the test would fail after ``timeout_secs`` seconds.
Dependency
----------
End-to-end ``'camera_assemble'``, ``'qr'``, ``'camera_assemble_qr'`` and
``'face'`` modes depend on OpenCV and numpy.
If not end-to-end mode, depend on OpenCV and device API
``cros.factory.device.camera``.
``'qr'`` and ``'camera_assemble_qr'`` mode also depend on library ``zbar``.
Examples
--------
To run a manual capture test:
.. test_list::
generic_camera_examples:CameraTests.FrontCameraManual
To run camera_assemble test, and specify the minimal luminance ratio to 0.7:
.. test_list::
generic_camera_examples:CameraTests.FrontCameraAssemble07
To run QR scan test, and specify camera resolution to 1920 x 1080:
.. test_list::
generic_camera_examples:CameraTests.FrontCameraQRScan1920x1080
To run camera_assemble_qr test:
.. test_list::
generic_camera_examples:CameraTests.FrontCameraAssembleQR
To run facial recognition test:
.. test_list::
generic_camera_examples:CameraTests.FrontCameraFace
To stress camera for 1000 seconds, and don't show the image:
.. test_list::
generic_camera_examples:CameraTests.FrontCameraStress
To stress camera capturing for 100 frames, have a timeout of 1000 seconds, and
don't show the image:
.. test_list::
generic_camera_examples:CameraTests.FrontCameraFrames
To check the camera capturing black frames (the maximum brightness less than
10), this is a subitem of testing camera privacy switch:
.. test_list::
generic_camera_examples:CameraTests.FrontCameraBrightness
This is used if camera_characteristics.conf is not ready. Users must replace
``camera_usb_vid_pid`` with vid pid they are testing:
.. test_list::
generic_camera_examples:CameraTests.CameraNoCharacteristics
To test the LED of the front camera:
.. test_list::
generic_camera_examples:CameraTests.FrontCameraLED
"""
import codecs
import enum
import logging
import numbers
import os
import queue
import random
import textwrap
import time
import uuid
from cros.factory.device.chromeos import camera
from cros.factory.device import device_utils
from cros.factory.test import i18n
from cros.factory.test.i18n import _
from cros.factory.test.rules import phase
from cros.factory.test import session
from cros.factory.test import test_case
from cros.factory.test.utils import barcode
from cros.factory.test.utils import camera_assemble
from cros.factory.test.utils import camera_utils
from cros.factory.utils.arg_utils import Arg
from cros.factory.utils import file_utils
from cros.factory.utils import schema
from cros.factory.utils import sync_utils
from cros.factory.external.py_lib import cv2 as cv
from cros.factory.external.py_lib import numpy as np
# Set JPEG image compression quality to 70 so that the image can be transferred
# through websocket.
_JPEG_QUALITY = 70
_HAAR_CASCADE_PATH = (
'/usr/local/share/opencv4/haarcascades/haarcascade_frontalface_default.xml')
MIN_LUMINANCE_RATIO_TABLE = {
camera_utils.CameraType.mipi: 0.7,
camera_utils.CameraType.usb: 0.5
}
WEB_API_MEDIA_STREAM_URL = (
'https://developer.mozilla.org/en-US/docs/Web/API/MediaStream')
CAMERA_REQUIREMENT_URL = (
'https://chromeos.google.com/partner/dlm/docs/versioned-requirements/'
'chromebook-12.2.html#cam-usrFcg-0003-v01')
class TestModes(str, enum.Enum):
camera_assemble = 'camera_assemble'
qr = 'qr'
camera_assemble_qr = 'camera_assemble_qr'
face = 'face'
timeout = 'timeout'
frame_count = 'frame_count'
manual = 'manual'
manual_led = 'manual_led'
brightness = 'brightness'
def __str__(self):
return self.name
_TEST_MODE_INST = {
TestModes.manual:
_('Press ENTER to pass or ESC to fail.'),
TestModes.timeout:
_('Running the camera until timeout.'),
TestModes.frame_count:
_('Running the camera until expected number of frames captured.'),
TestModes.camera_assemble:
_('Cover the field of view of the camera with a white paper. '
'The red grids represent which region is too dark.'),
TestModes.qr:
_('Scanning QR code...'),
TestModes.camera_assemble_qr:
_('Place QR code in the frame and cover the field of view of the'
' camera with a white paper. The red grids represent which region is'
' too dark.'),
TestModes.face:
_('Detecting faces...'),
TestModes.brightness:
_('Checking brightness...')
}
_RANGE_SCHEMA = schema.JSONSchemaDict(
'threshold schema object',
{
'type': 'array',
'items': {
'type': ['number', 'null']
},
'minItems': 2,
'maxItems': 2
},
)
class CameraTest(test_case.TestCase):
"""Main class for camera test."""
related_components = (
test_case.TestCategory.CAMERA,
test_case.TestCategory.MIPI_CAMERA,
)
ARGS = [
Arg('mode', TestModes, 'The test mode to test camera.', default='qr'),
Arg(
'num_frames_to_pass', int,
'The number of frames with faces in mode "face", '
'QR code presented in mode "qr", '
'or any frames in mode "frame_count" to pass the test.', default=10),
Arg(
'process_rate', numbers.Real,
'The process rate of face recognition or '
'QR code scanning in times per second.', default=5),
Arg('QR_string', str, 'Encoded string in QR code.',
default='Hello ChromeOS!'),
Arg(
'brightness_range', list, '**[min, max]**, check if the maximum '
'brightness is between [min, max] (inclusive). None means no limit.',
default=[None, None], schema=_RANGE_SCHEMA),
Arg('capture_fps', numbers.Real,
'Camera capture rate in frames per second.', default=30),
Arg('timeout_secs', int, 'Timeout value for the test.', default=20),
Arg('show_image', bool, 'Whether to actually show the image on screen.',
default=True),
Arg(
'e2e_mode', bool,
textwrap.dedent(f"""
Perform end-to-end test or not (for camera).
In non-e2e mode, camera data is grabbed from video device by OpenCV.
In e2e mode, camera data is directly streamed on frontend using
JavaScript `MediaStream API <{WEB_API_MEDIA_STREAM_URL}>`_.
In e2e mode, if the test fails to create a video, then run the Tast
test camera.GetUserMedia.real to test the e2e readiness. If it fails,
for USB cameras, you can set e2e_mode to false and try the non-e2e
mode; for MIPI cameras, it means the function is not ready for using
this pytest.
"""), default=True),
Arg(
'resize_ratio', float,
'The resize ratio of captured image on screen, '
'has no effect on e2e mode.', default=0.4),
Arg('camera_facing', camera_utils.CameraFacing,
('String "front" or "rear" for the camera to test. '
'If in normal mode, default is automatically searching one. '
'If in e2e mode, default is "front".'), default=None),
Arg('camera_usb_vid_pid', list,
('**[vid, pid]** '
'The USB vendor id and product id of the camera to test. '
'Each is a hex string. '
'For testing an external USB camera. '
'Only valid in normal mode and if camera_facing is not selected. '),
default=None),
Arg(
'flip_image', bool,
'Whether to flip the image horizontally. This should be set to False'
'for the rear facing camera so the displayed image looks correct.'
'The default value is False if camera_facing is "rear", True '
'otherwise.', default=None),
Arg(
'camera_args', dict,
textwrap.dedent(f"""
Args used for enabling the camera device.
In non-e2e mode, we call ``EnableCamera(**camera_args)`` defined
in ``camera_utils.ICameraReader``.
In e2e mode, only "resolution" is used. The default resolution is
``[1280, 720]``. See the hardware
`requirement <{CAMERA_REQUIREMENT_URL}>`_."""), default={}),
Arg('flicker_interval_secs', (int, float),
'The flicker interval in seconds in manual_led mode', default=0.5),
Arg('fullscreen', bool, 'Run the test in fullscreen', default=False),
Arg('video_start_play_timeout_ms', int,
'The timeout between we open a stream and it starts to play.',
default=5000),
Arg('get_user_media_retries', int,
('The times that we try to getUserMedia in camera.js. The '
'getUserMedia executes at most (1+get_user_media_retries) times.'),
default=0),
Arg('reinitialization_delay_ms', int,
'The delay between disable and enable in camera.js.', default=5000),
Arg('min_luminance_ratio', float,
('The minimal acceptable luminance of the boundary region of an'
'image. This value is multiplied by the brightest region of an'
'image. If the luminance of the boundary region is lower than or'
'equal to the product, we consider the image containing black edges,'
'and thus the camera is badly assembled. It is recommended to set'
'this value to 0.5 for USB camera and 0.7 for MIPI camera.'),
default=0.5)
]
def GetCamera(self) -> camera.ChromeOSCamera:
return self.dut.camera
def _Timeout(self):
if self.mode == TestModes.timeout:
# If it keeps capturing images until timeout, the test passes.
self.PassTask()
else:
self.FailTask('Camera test failed due to timeout.')
def ShowFeedback(self, msg):
# yapf: disable
self.ui.CallJSFunction('showFeedback', msg) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
def AppendFeedback(self, msg):
# yapf: disable
self.ui.CallJSFunction('appendFeedback', msg) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
def ShowInstruction(self, msg):
# yapf: disable
self.ui.CallJSFunction('showInstruction', msg) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
def _RunJSBlockingImpl(self, js, func):
# yapf: disable
return_queue = queue.Queue() # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
event_name = f'wait_js_{func}_{uuid.uuid4()}'
# yapf: disable
self.event_loop.AddEventHandler( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
event_name, lambda event: return_queue.put(event.data))
# yapf: disable
self.ui.CallJSFunction(func, js, event_name) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
ret = sync_utils.QueueGet(return_queue)
# yapf: disable
self.event_loop.RemoveEventHandler(event_name) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
if 'error' in ret:
self.FailTask(ret['error'])
return ret['data']
# TODO(pihsun): Put this in test_ui.
def RunJSBlocking(self, js):
self._RunJSBlockingImpl(js, 'runJS')
# TODO(pihsun): Put this in test_ui.
def RunJSPromiseBlocking(self, js):
return self._RunJSBlockingImpl(js, 'runJSPromise')
def EnableDevice(self):
if self.e2e_mode:
self.RunJSPromiseBlocking('cameraTest.enable()')
else:
# yapf: disable
self.camera_device.EnableCamera(**self.args.camera_args) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
def DisableDevice(self):
if self.e2e_mode:
self.RunJSBlocking('cameraTest.disable()')
else:
# yapf: disable
self.camera_device.DisableCamera() # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
def ReadSingleFrame(self):
if self.e2e_mode:
if self.need_transmit_from_ui:
# TODO(pihsun): The shape detection API (face / barcode detection) are
# not implemented on desktop Chrome yet. We don't need to transmit the
# image back after these APIs are implemented, and can do all
# postprocessing on JavaScript.
blob_path = self.RunJSPromiseBlocking(
'cameraTest.grabFrameAndTransmitBack()')
blob = codecs.decode(
file_utils.ReadFile(blob_path, encoding=None), 'base64')
os.unlink(blob_path)
# yapf: disable
return cv.imdecode(np.frombuffer(blob, dtype=np.uint8), cv.IMREAD_COLOR) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.RunJSPromiseBlocking('cameraTest.grabFrame()')
return None
# yapf: disable
return self.camera_device.ReadSingleFrame() # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
def LEDTest(self):
flicker = bool(random.randint(0, 1))
# yapf: disable
self.ui.BindStandardFailKeys() # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
for i in range(2):
if i == flicker:
# yapf: disable
self.ui.BindKey(str(i), lambda unused_event: self.PassTask()) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
else:
# yapf: disable
self.ui.BindKey( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
str(i), lambda unused_event: self.FailTask('Wrong key pressed.'))
self.ShowInstruction(
_('Press 0 if LED is constantly lit, 1 if LED is flickering,\n'
'or ESC to fail.'))
# yapf: disable
self.ui.CallJSFunction('hideImage') # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
if flicker:
while True:
# Flickers the LED
self.EnableDevice()
self.ReadSingleFrame()
# yapf: disable
self.Sleep(self.args.flicker_interval_secs) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.DisableDevice()
# yapf: disable
self.Sleep(self.args.flicker_interval_secs) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
else:
# Constantly lights the LED
self.EnableDevice()
while True:
self.ReadSingleFrame()
self.Sleep(0.5)
def _DrawRectangle(self, cv_image, rect_pos, rect_shape, color, fill):
"""Draw rectangles on UI.
Args:
cv_image: The image captured by camera.
rect_pos: The x, y coordinates of the top-left corner of the rectangle.
rect_shape: The width and height of the rectangle.
color: The color string and its corresponding BGR value.
fill: Fill the rectangle of not.
Returns:
The js functions used to draw the rectangles.
"""
x_pos, y_pos = rect_pos
rect_width, rect_height = rect_shape
color_string, bgr_color = color
image_height, image_width = cv_image.shape[:2]
draw_rect_js = ''
# yapf: disable
thickness = cv.FILLED if fill else 1 # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
fill_string = 'true' if fill else 'false'
if self.e2e_mode:
# Normalize the coordinates / size to [0, 1], since the canvas in the
# frontend may not be the same size as the image.
draw_rect_js += (
f'cameraTest.drawRect({float(x_pos) / image_width}, '
f'{float(y_pos) / image_height}, {float(rect_width) / image_width}, '
f'{float(rect_height) / image_height}, "{color_string}", '
f'{fill_string});')
else:
# yapf: disable
cv.rectangle(cv_image, (x_pos, y_pos), # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
(x_pos + rect_width, y_pos + rect_height), bgr_color,
thickness)
return draw_rect_js
def DetectFaces(self, cv_image):
# TODO(pihsun): Use the shape detection API in Chrome in e2e mode when it
# is ready.
height, width = cv_image.shape[:2]
# yapf: disable
cascade = cv.CascadeClassifier(_HAAR_CASCADE_PATH) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
detected_objs = cascade.detectMultiScale(
cv_image, scaleFactor=1.2, minNeighbors=2,
# yapf: disable
flags=cv.CASCADE_DO_CANNY_PRUNING, minSize=(width // 10, height // 10)) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# Detected_objs will be numpy array or an empty tuple. bool(numpy_array)
# will not work (will raise an exception).
detected = len(detected_objs) > 0
draw_rect_js = 'cameraTest.clearOverlay();'
for x, y, w, h in detected_objs:
draw_rect_js += self._DrawRectangle(cv_image, (x, y), (w, h),
('white', 255), False)
if self.e2e_mode:
self.RunJSBlocking(draw_rect_js)
return detected
def DetectAssemblyIssue(self, cv_image):
if self.min_luminance_ratio < MIN_LUMINANCE_RATIO_TABLE[self.camera_type]:
logging.warning('min_luminance_ratio for %s camera should be at least %s',
self.camera_type,
MIN_LUMINANCE_RATIO_TABLE[self.camera_type])
camera_assemble_issue = camera_assemble.DetectCameraAssemblyIssue(
cv_image, self.min_luminance_ratio)
is_too_dark, grid, grid_size = \
camera_assemble_issue.IsBoundaryRegionTooDark()
if is_too_dark:
grid_width, grid_height = grid_size
height, width = cv_image.shape[:2]
draw_rect_js = 'cameraTest.clearOverlay();'
for grid_r, y_pos in enumerate(range(0, height, grid_height)):
for grid_c, x_pos in enumerate(range(0, width, grid_width)):
if grid[grid_r][grid_c]:
# It will be slow if we call the js function for each grid.
# Instead, we run the js functions all at once at the end of the
# loop.
draw_rect_js += self._DrawRectangle(cv_image, (x_pos, y_pos),
(grid_width, grid_height),
('red', (0, 0, 255)), True)
if self.e2e_mode:
self.RunJSBlocking(draw_rect_js)
return not is_too_dark
def ScanQRCode(self, cv_image):
scanned_text = None
# TODO(pihsun): Use the shape detection API in Chrome in e2e mode when it
# is ready. Note that the detection region is different for front and rear
# cameras in camera_assemble_qr mode. Since the front camera image will be
# flipped, the detection region for front camera is at the right half, and
# that of the rear camera is at the left half.
scan_results = barcode.ScanQRCode(cv_image)
if scan_results:
scanned_text = scan_results[0]
if scanned_text:
self.ShowFeedback(
i18n.StringFormat(_('Scanned QR code: "{text}"'), text=scanned_text))
# yapf: disable
if scanned_text != self.args.QR_string: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
logging.warning(
'Scanned QR code "%s" does not match target QR code "%s"',
# yapf: disable
scanned_text, self.args.QR_string) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
return scanned_text == self.args.QR_string # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
def GetResultString(self, result):
return _('Success!') if result else _('Failure')
def DetectAssemblyIssueAndScanQRCode(self, cv_image):
camera_well_assembled = self.DetectAssemblyIssue(cv_image)
img_height, img_width = cv_image.shape[:2]
x_pos, y_pos, qr_width, qr_height = \
camera_assemble.GetQRCodeDetectionRegion(img_height, img_width)
# Since we'll use the center and boundary regions of the image when
# conducting the camera_assemble test, we restrict the position of the QR
# code so that it won't be at the center or boundary regions.
qr_region = cv_image[y_pos:y_pos + qr_height, x_pos:x_pos + qr_width, :]
qr_code_scan_success = self.ScanQRCode(qr_region)
# yapf: disable
if self.args.show_image: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.DrawQRDetectionRegion(cv_image)
string_to_show = i18n.StringFormat(
_(
'Camera assemble: {camera_well_assembled}, '
'QR code: {qr_code_scan_success}',
camera_well_assembled=self.GetResultString(camera_well_assembled),
qr_code_scan_success=self.GetResultString(qr_code_scan_success)))
if qr_code_scan_success:
self.AppendFeedback(string_to_show)
else:
self.ShowFeedback(string_to_show)
return camera_well_assembled and qr_code_scan_success
def BrightnessCheck(self, cv_image):
# yapf: disable
value = cv.cvtColor(cv_image, cv.COLOR_BGR2GRAY).max() # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
threshold = self.args.brightness_range # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
session.console.info(f'Maximum brightness: {value}')
return ((threshold[0] is None or threshold[0] <= value) and
(threshold[1] is None or value <= threshold[1]))
def ShowImage(self, cv_image):
if self.e2e_mode:
# In e2e mode, the image is directly shown by frontend in a video
# element, independent to the calls to ShowImage here.
return
# yapf: disable
resize_ratio = self.args.resize_ratio # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
cv_image = cv.resize(cv_image, None, fx=resize_ratio, fy=resize_ratio, # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
interpolation=cv.INTER_AREA) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
if self.flip_image:
# yapf: disable
cv_image = cv.flip(cv_image, 1) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
unused_retval, jpg_data = cv.imencode( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
'.jpg', cv_image, (cv.IMWRITE_JPEG_QUALITY, _JPEG_QUALITY)) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
jpg_base64 = codecs.encode(jpg_data.tobytes(), 'base64')
try:
# TODO(pihsun): Don't use CallJSFunction for transmitting image back
# to UI. Use URLForData instead, since event server actually
# broadcast to all client, and is not suitable for large amount of
# data.
# yapf: disable
self.ui.CallJSFunction( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
'showImage', 'data:image/jpeg;base64,' + jpg_base64.decode('utf-8'))
except AttributeError:
# The websocket is closed because test has passed/failed.
return
def DrawQRDetectionRegion(self, cv_image):
img_height, img_width = cv_image.shape[:2]
x_pos, y_pos, qr_width, qr_height = \
camera_assemble.GetQRCodeDetectionRegion(img_height, img_width)
if self.e2e_mode:
self.RunJSBlocking('cameraTest.clearOverlay()')
self.RunJSBlocking(
f'cameraTest.drawRect({float(x_pos) / img_width}, '
f'{float(y_pos) / img_height}, {float(qr_width) / img_width},'
f' {float(qr_height) / img_height})')
else:
# yapf: disable
cv.rectangle(cv_image, (x_pos, y_pos), # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
(x_pos + qr_width, y_pos + qr_height), 255)
def CaptureTestFrame(self, mode, cv_image):
if mode == TestModes.frame_count:
return True
if mode == TestModes.camera_assemble:
return self.DetectAssemblyIssue(cv_image)
if mode == TestModes.qr:
return self.ScanQRCode(cv_image)
if mode == TestModes.camera_assemble_qr:
return self.DetectAssemblyIssueAndScanQRCode(cv_image)
if mode == TestModes.face:
return self.DetectFaces(cv_image)
if mode == TestModes.brightness:
return self.BrightnessCheck(cv_image)
# For all other test, like TestModes.manually, return False.
return False
def CaptureTest(self, mode):
self.ShowInstruction(_TEST_MODE_INST[mode])
if mode == TestModes.manual:
# yapf: disable
self.ui.BindStandardKeys() # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
if not self.args.show_image: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.ui.CallJSFunction('hideImage') # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.EnableDevice()
try:
frame_count = 0
# yapf: disable
frame_interval = 1.0 / float(self.args.capture_fps) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
last_process_time = time.time()
if mode == TestModes.frame_count:
process_interval = 0
else:
# yapf: disable
process_interval = 1.0 / float(self.args.process_rate) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
while True:
start_time = time.time()
cv_image = self.ReadSingleFrame()
if time.time() - last_process_time > process_interval:
last_process_time = time.time()
if self.CaptureTestFrame(mode, cv_image):
frame_count += 1
# yapf: disable
if frame_count >= self.args.num_frames_to_pass: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
return
# yapf: disable
if self.args.show_image: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.ShowImage(cv_image)
self.Sleep(frame_interval - (time.time() - start_time))
finally:
self.DisableDevice()
def setUp(self):
self.dut = device_utils.CreateDUTInterface()
# Set cv2 logging level to ERROR to avoid noise.
cv.setLogLevel(0) # type: ignore #TODO(b/338318729) Fixit!
# yapf: disable
self.mode = self.args.mode # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
if self.args.camera_facing is None: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.camera_type = camera_utils.CameraType.usb
self.assertTrue(
# yapf: disable
self.args.camera_usb_vid_pid is not None, # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
'camera_usb_vid_pid must be set if camera_facing is None')
else:
self.camera_type = camera_utils.GetCameraTypeFromCameraFacing(
# yapf: disable
self.args.camera_facing) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.e2e_mode = self.args.e2e_mode # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.min_luminance_ratio = self.args.min_luminance_ratio # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# Whether we need to transmit image from UI back to Python in e2e mode.
# TODO(pihsun): This can be removed after the desktop Chrome implements
# shape detection API.
self.need_transmit_from_ui = False
if self.camera_type == camera_utils.CameraType.mipi:
self.assertTrue(self.e2e_mode,
'e2e_mode should be enabled for MIPI camera.')
# yapf: disable
self.flip_image = self.args.flip_image # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
if self.flip_image is None:
# yapf: disable
self.flip_image = self.args.camera_facing != 'rear' # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
if self.args.fullscreen: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.ui.RunJS('test.setFullScreen(true)') # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
if self.e2e_mode:
if not self.dut.link.IsLocal():
raise ValueError('e2e mode does not work on remote DUT.')
if self.mode == TestModes.frame_count:
logging.warning('frame count mode is NOT real frame count in e2e mode, '
'consider using timeout instead.')
# yapf: disable
camera_facing = ('front' if self.args.camera_facing is None else # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.args.camera_facing) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
options = {
'facingMode': {
'front': 'user',
'rear': 'environment'
}[camera_facing],
# yapf: disable
'videoStartPlayTimeoutMs': self.args.video_start_play_timeout_ms, # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
'getUserMediaRetries': self.args.get_user_media_retries, # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
'reinitializationDelayMs': self.args.reinitialization_delay_ms, # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
}
# yapf: disable
resolution = self.args.camera_args.get('resolution', (1280, 720)) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
options['width'], options['height'] = resolution
options['flipImage'] = self.flip_image
# yapf: disable
self.ui.RunJS( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
'window.cameraTest = new CameraTest(args.options)', options=options)
self.camera_device = None
self.need_transmit_from_ui = self.mode in (
TestModes.camera_assemble,
TestModes.qr,
TestModes.camera_assemble_qr,
TestModes.face,
TestModes.brightness,
)
# yapf: disable
elif (self.args.camera_facing is None and # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.args.camera_usb_vid_pid is not None and # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
len(self.args.camera_usb_vid_pid) == 2): # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.camera_device = self.GetCamera().GetCameraDeviceByUsbVidPid(
# yapf: disable
self.args.camera_usb_vid_pid[0], self.args.camera_usb_vid_pid[1]) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
else:
self.camera_device = self.GetCamera().GetCameraDevice(
# yapf: disable
self.args.camera_facing) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
def runTest(self):
# yapf: disable
self.ui.StartCountdownTimer(self.args.timeout_secs, self._Timeout) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
if self.mode == TestModes.manual:
self.assertFalse(phase.GetPhase() > phase.DVT,
msg='"manual" mode cannot be used after DVT')
if self.mode in [
TestModes.manual, TestModes.camera_assemble, TestModes.qr,
TestModes.camera_assemble_qr, TestModes.face
]:
# yapf: disable
self.assertTrue(self.args.show_image, # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
msg='show_image should be set to true!')
if self.mode == TestModes.manual_led:
self.LEDTest()
else:
self.CaptureTest(self.mode)