| // Copyright 2021 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "camera3_test/camera3_preview_fixture.h" |
| #include "camera3_test/camera3_still_capture_fixture.h" |
| |
| #include <unistd.h> |
| |
| #include <base/bind.h> |
| #include <base/command_line.h> |
| #include <base/files/file_util.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <libyuv.h> |
| |
| namespace camera3_test { |
| |
| // Test parameters: |
| // - Camera ID |
| class Camera3FaceDetectionTest : public Camera3PreviewFixture, |
| public ::testing::WithParamInterface<int32_t> { |
| public: |
| const uint32_t kNumPreviewFrames = 20; |
| const uint32_t kTimeoutMsPerFrame = 1000; |
| Camera3FaceDetectionTest() |
| : Camera3PreviewFixture(std::vector<int>(1, GetParam())), |
| cam_id_(GetParam()), |
| expected_num_faces_(GetCommandLineFaceDetectNumber()) {} |
| |
| protected: |
| void SetUp() override; |
| |
| void ProcessPreviewResult(int cam_id, |
| uint32_t frame_number, |
| ScopedCameraMetadata metadata); |
| |
| int GetCommandLineFaceDetectNumber() { |
| std::string switch_value = |
| base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| "expected_num_faces"); |
| if (switch_value == "") { |
| return -1; |
| } |
| int value; |
| if (!base::StringToInt(switch_value, &value)) { |
| LOG(ERROR) << "Failed to convert " << switch_value << " to int"; |
| return -1; |
| } |
| if (value < 0) { |
| return -1; |
| } |
| return value; |
| } |
| |
| void CheckNumOfFaces(int num_faces); |
| |
| int cam_id_; |
| int expected_num_faces_; |
| |
| private: |
| ScopedCameraMetadata result_metadata_; |
| }; |
| |
| void Camera3FaceDetectionTest::SetUp() { |
| ASSERT_EQ(0, cam_service_.Initialize( |
| Camera3Service::ProcessStillCaptureResultCallback(), |
| Camera3Service::ProcessRecordingResultCallback(), |
| base::BindRepeating( |
| &Camera3FaceDetectionTest::ProcessPreviewResult, |
| base::Unretained(this)))) |
| << "Failed to initialize camera service"; |
| } |
| |
| void Camera3FaceDetectionTest::ProcessPreviewResult( |
| int cam_id, uint32_t /*frame_number*/, ScopedCameraMetadata metadata) { |
| VLOGF_ENTER(); |
| result_metadata_ = std::move(metadata); |
| } |
| |
| void Camera3FaceDetectionTest::CheckNumOfFaces(int num_faces) { |
| ASSERT_NE(nullptr, result_metadata_.get()) |
| << "Result metadata is unavailable"; |
| camera_metadata_ro_entry_t entry; |
| int result = find_camera_metadata_ro_entry( |
| result_metadata_.get(), ANDROID_STATISTICS_FACE_RECTANGLES, &entry); |
| // Accept no rectangles. |
| if (num_faces == 0 && result != 0) { |
| return; |
| } |
| ASSERT_EQ(0, result) |
| << "Metadata key ANDROID_STATISTICS_FACE_RECTANGLES not found"; |
| EXPECT_EQ(num_faces * 4, entry.count) |
| << "Expect face rectangles size " << num_faces * 4 << " but detected " |
| << entry.count; |
| ASSERT_EQ( |
| 0, find_camera_metadata_ro_entry(result_metadata_.get(), |
| ANDROID_STATISTICS_FACE_SCORES, &entry)) |
| << "Metadata key ANDROID_STATISTICS_FACE_SCORES not found"; |
| EXPECT_EQ(num_faces, entry.count) |
| << "Expect " << num_faces << " faces, but detected " << entry.count |
| << " faces"; |
| result_metadata_.reset(); |
| } |
| |
| TEST_P(Camera3FaceDetectionTest, Detection) { |
| // Run only if --expected_num_faces argument presented. |
| if (expected_num_faces_ < 0) { |
| GTEST_SKIP(); |
| } |
| |
| auto IsAFSupported = [this]() { |
| std::vector<uint8_t> available_af_modes; |
| cam_service_.GetStaticInfo(cam_id_)->GetAvailableAFModes( |
| &available_af_modes); |
| uint8_t af_modes[] = {ANDROID_CONTROL_AF_MODE_AUTO, |
| ANDROID_CONTROL_AF_MODE_CONTINUOUS_PICTURE, |
| ANDROID_CONTROL_AF_MODE_CONTINUOUS_VIDEO, |
| ANDROID_CONTROL_AF_MODE_MACRO}; |
| for (const auto& it : af_modes) { |
| if (std::find(available_af_modes.begin(), available_af_modes.end(), it) != |
| available_af_modes.end()) { |
| return true; |
| } |
| } |
| return false; |
| }; |
| |
| ASSERT_TRUE(cam_service_.GetStaticInfo(cam_id_)->IsKeyAvailable( |
| ANDROID_STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES)) |
| << "NO ANDROID_STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES key in static " |
| "info"; |
| std::set<uint8_t> face_detect_modes; |
| ASSERT_EQ(0, cam_service_.GetStaticInfo(cam_id_)->GetAvailableFaceDetectModes( |
| &face_detect_modes) != 0) |
| << "Failed to get face detect modes"; |
| ASSERT_NE(face_detect_modes.find(ANDROID_STATISTICS_FACE_DETECT_MODE_SIMPLE), |
| face_detect_modes.end()) |
| << "Can't find ANDROID_STATISTICS_FACE_DETECT_MODE_SIMPLE"; |
| |
| auto resolution = |
| cam_service_.GetStaticInfo(cam_id_) |
| ->GetSortedOutputResolutions(HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED) |
| .back(); |
| ResolutionInfo jpeg_resolution(0, 0), recording_resolution(0, 0); |
| ASSERT_EQ(0, cam_service_.StartPreview(cam_id_, resolution, jpeg_resolution, |
| recording_resolution)) |
| << "Starting preview fails"; |
| |
| // Trigger an auto focus run, and wait for AF locked. |
| if (IsAFSupported()) { |
| cam_service_.StartAutoFocus(cam_id_); |
| ASSERT_EQ(0, cam_service_.WaitForAutoFocusDone(cam_id_)) |
| << "Wait for auto focus done timed out"; |
| } |
| // Wait for AWB converged, then lock it. |
| ASSERT_EQ(0, cam_service_.WaitForAWBConvergedAndLock(cam_id_)) |
| << "Wait for AWB converged timed out"; |
| |
| // Trigger an AE precapture metering sequence and wait for AE converged. |
| cam_service_.StartAEPrecapture(cam_id_); |
| ASSERT_EQ(0, cam_service_.WaitForAEStable(cam_id_)) |
| << "Wait for AE stable timed out"; |
| |
| // Check there is no face detected before enabling face detection |
| ASSERT_EQ(0, cam_service_.WaitForPreviewFrames(cam_id_, kNumPreviewFrames, |
| kTimeoutMsPerFrame)); |
| CheckNumOfFaces(0); |
| |
| cam_service_.StartFaceDetection(cam_id_); |
| ASSERT_EQ(0, cam_service_.WaitForPreviewFrames(cam_id_, kNumPreviewFrames, |
| kTimeoutMsPerFrame)); |
| CheckNumOfFaces(expected_num_faces_); |
| |
| // Check no face detected after stop face detection |
| cam_service_.StopFaceDetection(cam_id_); |
| ASSERT_EQ(0, cam_service_.WaitForPreviewFrames(cam_id_, kNumPreviewFrames, |
| kTimeoutMsPerFrame)); |
| CheckNumOfFaces(0); |
| |
| cam_service_.StopPreview(cam_id_); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| Camera3FaceTest, |
| Camera3FaceDetectionTest, |
| ::testing::ValuesIn(Camera3Module().GetTestCameraIds())); |
| |
| // Test parameters: |
| // - Camera ID |
| class Camera3FaceAutoExposureTest |
| : public Camera3StillCaptureFixture, |
| public ::testing::WithParamInterface<int32_t> { |
| public: |
| // we require face auto exposure works in 90 frames. |
| const uint32_t kNumPreviewFrames = 90; |
| const uint32_t kTimeoutMsPerFrame = 1000; |
| |
| Camera3FaceAutoExposureTest() |
| : Camera3StillCaptureFixture(std::vector<int>(1, GetParam())), |
| cam_id_(GetParam()), |
| expected_num_faces_(GetCommandLineFaceDetectNumber()), |
| dump_path_(GetCommandLineDumpPath()) {} |
| |
| protected: |
| void SetUp() override; |
| void TearDown() override; |
| |
| void ProcessPreviewResult(int cam_id, |
| uint32_t frame_number, |
| ScopedCameraMetadata metadata); |
| |
| struct ImageI420 { |
| ImageI420(uint32_t w, uint32_t h); |
| int SaveToFile(base::FilePath file_path) const; |
| |
| uint32_t width; |
| uint32_t height; |
| std::vector<uint8_t> data; |
| uint32_t size; |
| |
| uint8_t* y_addr; |
| uint8_t* u_addr; |
| uint8_t* v_addr; |
| |
| uint32_t y_stride; |
| uint32_t u_stride; |
| uint32_t v_stride; |
| }; |
| using ScopedImageI420 = std::unique_ptr<ImageI420>; |
| |
| ScopedImageI420 ConvertJpegToI420(const cros::ScopedBufferHandle& buffer, |
| int width, |
| int height); |
| |
| // Get the Luma of the face. |enable_face_ae| indicates that we get the Luma |
| // value from face auto exposure or not. If there are many faces detected, |
| // only gets the luma value of the first face reported from camera hal. |
| void GetFaceLumaValue(bool enable_face_ae); |
| void Do3AConverged(); |
| |
| inline base::FilePath GetCommandLineDumpPath() { |
| return base::CommandLine::ForCurrentProcess()->GetSwitchValuePath( |
| "dump_path"); |
| } |
| |
| int GetCommandLineFaceDetectNumber() { |
| std::string switch_value = |
| base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| "expected_num_faces"); |
| if (switch_value == "") { |
| return -1; |
| } |
| int value; |
| if (!base::StringToInt(switch_value, &value)) { |
| LOG(ERROR) << "Failed to convert " << switch_value << " to int"; |
| return -1; |
| } |
| if (value < 0) { |
| return -1; |
| } |
| return value; |
| } |
| |
| int cam_id_; |
| int expected_num_faces_; |
| base::FilePath dump_path_; |
| |
| private: |
| ScopedCameraMetadata preview_result_metadata_; |
| }; |
| |
| void Camera3FaceAutoExposureTest::SetUp() { |
| ASSERT_EQ(0, cam_service_.Initialize( |
| base::BindRepeating( |
| &Camera3StillCaptureFixture::ProcessStillCaptureResult, |
| base::Unretained(this)), |
| Camera3Service::ProcessRecordingResultCallback(), |
| base::BindRepeating( |
| &Camera3FaceAutoExposureTest::ProcessPreviewResult, |
| base::Unretained(this)))) |
| << "Failed to initialize camera service"; |
| } |
| |
| void Camera3FaceAutoExposureTest::TearDown() { |
| cam_service_.Destroy(); |
| } |
| |
| void Camera3FaceAutoExposureTest::ProcessPreviewResult( |
| int cam_id, uint32_t /*frame_number*/, ScopedCameraMetadata metadata) { |
| VLOGF_ENTER(); |
| preview_result_metadata_ = std::move(metadata); |
| } |
| |
| #define DIV_ROUND_UP(n, d) (((n) + (d)-1) / (d)) |
| Camera3FaceAutoExposureTest::ImageI420::ImageI420(uint32_t w, uint32_t h) |
| : width(w), height(h) { |
| y_stride = DIV_ROUND_UP(w, 2) * 2; |
| u_stride = DIV_ROUND_UP(w, 2); |
| v_stride = DIV_ROUND_UP(w, 2); |
| size_t y_plane_size = y_stride * height; |
| size = y_plane_size * 3 / 2; |
| data.resize(size); |
| y_addr = data.data(); |
| u_addr = y_addr + y_plane_size; |
| v_addr = u_addr + y_plane_size / 4; |
| } |
| |
| int Camera3FaceAutoExposureTest::ImageI420::SaveToFile( |
| base::FilePath file_path) const { |
| if (base::WriteFile(file_path, reinterpret_cast<const char*>(data.data()), |
| size) != size) { |
| LOGF(ERROR) << "Failed to write file " << file_path; |
| return -EINVAL; |
| } |
| return 0; |
| } |
| |
| Camera3FaceAutoExposureTest::ScopedImageI420 |
| Camera3FaceAutoExposureTest::ConvertJpegToI420( |
| const cros::ScopedBufferHandle& buffer, int width, int height) { |
| auto gralloc = Camera3TestGralloc::GetInstance(); |
| buffer_handle_t handle = *buffer; |
| if (gralloc->GetFormat(handle) != HAL_PIXEL_FORMAT_BLOB) { |
| LOGF(ERROR) << "Invalid format"; |
| return ScopedImageI420(nullptr); |
| } |
| size_t jpeg_max_size = cam_service_.GetStaticInfo(cam_id_)->GetJpegMaxSize(); |
| void* buf_addr = nullptr; |
| if (gralloc->Lock(handle, 0, 0, 0, jpeg_max_size, 1, &buf_addr)) { |
| LOGF(ERROR) << "Failed to lock buffer"; |
| return ScopedImageI420(nullptr); |
| } |
| auto jpeg_blob = reinterpret_cast<camera3_jpeg_blob_t*>( |
| static_cast<uint8_t*>(buf_addr) + jpeg_max_size - |
| sizeof(camera3_jpeg_blob_t)); |
| if (static_cast<void*>(jpeg_blob) < buf_addr || |
| jpeg_blob->jpeg_blob_id != CAMERA3_JPEG_BLOB_ID) { |
| gralloc->Unlock(handle); |
| LOGF(ERROR) << "Invalid JPEG BLOB ID"; |
| return ScopedImageI420(nullptr); |
| } |
| |
| ScopedImageI420 i420(new ImageI420(width, height)); |
| if (libyuv::MJPGToI420(static_cast<uint8_t*>(buf_addr), jpeg_blob->jpeg_size, |
| i420->y_addr, i420->y_stride, i420->u_addr, |
| i420->u_stride, i420->v_addr, i420->v_stride, width, |
| height, width, height) != 0) { |
| LOGF(ERROR) << "Failed to convert image from JPEG"; |
| gralloc->Unlock(handle); |
| return ScopedImageI420(nullptr); |
| } |
| gralloc->Unlock(handle); |
| |
| return i420; |
| } |
| |
| void Camera3FaceAutoExposureTest::Do3AConverged() { |
| auto IsAFSupported = [this]() { |
| std::vector<uint8_t> available_af_modes; |
| cam_service_.GetStaticInfo(cam_id_)->GetAvailableAFModes( |
| &available_af_modes); |
| uint8_t af_modes[] = {ANDROID_CONTROL_AF_MODE_AUTO, |
| ANDROID_CONTROL_AF_MODE_CONTINUOUS_PICTURE, |
| ANDROID_CONTROL_AF_MODE_CONTINUOUS_VIDEO, |
| ANDROID_CONTROL_AF_MODE_MACRO}; |
| for (const auto& it : af_modes) { |
| if (std::find(available_af_modes.begin(), available_af_modes.end(), it) != |
| available_af_modes.end()) { |
| return true; |
| } |
| } |
| return false; |
| }; |
| |
| // Trigger an auto focus run, and wait for AF locked. |
| if (IsAFSupported()) { |
| cam_service_.StartAutoFocus(cam_id_); |
| int af_result = cam_service_.WaitForAutoFocusDone(cam_id_); |
| if (af_result != 0) { |
| LOGF(WARNING) << "Ignore AF converged timeout failure."; |
| } |
| } |
| // Wait for AWB converged, then lock it. |
| int awb_result = cam_service_.WaitForAWBConvergedAndLock(cam_id_); |
| if (awb_result != 0) { |
| LOGF(WARNING) << "Ignore AWB converged timeout failure."; |
| } |
| // Trigger an AE precapture metering sequence and wait for AE converged. |
| cam_service_.StartAEPrecapture(cam_id_); |
| int ae_result = cam_service_.WaitForAEStable(cam_id_); |
| if (ae_result != 0) { |
| LOGF(WARNING) << "Ignore AE converged timeout failure."; |
| } |
| } |
| |
| void Camera3FaceAutoExposureTest::GetFaceLumaValue(bool enable_face_ae) { |
| ASSERT_TRUE(cam_service_.GetStaticInfo(cam_id_)->IsKeyAvailable( |
| ANDROID_STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES)) |
| << "NO ANDROID_STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES key in static " |
| "info"; |
| std::set<uint8_t> face_detect_modes; |
| ASSERT_EQ(0, cam_service_.GetStaticInfo(cam_id_)->GetAvailableFaceDetectModes( |
| &face_detect_modes) != 0) |
| << "Failed to get face detect modes"; |
| ASSERT_NE(face_detect_modes.find(ANDROID_STATISTICS_FACE_DETECT_MODE_SIMPLE), |
| face_detect_modes.end()) |
| << "Can't find ANDROID_STATISTICS_FACE_DETECT_MODE_SIMPLE"; |
| |
| // Get the max resolution. |
| ResolutionInfo jpeg_resolution = |
| cam_service_.GetStaticInfo(cam_id_) |
| ->GetSortedOutputResolutions(HAL_PIXEL_FORMAT_BLOB) |
| .back(); |
| ResolutionInfo preview_resolution = |
| cam_service_.GetStaticInfo(cam_id_) |
| ->GetSortedOutputResolutions(HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED) |
| .back(); |
| ResolutionInfo recording_resolution(0, 0); |
| ASSERT_EQ( |
| 0, cam_service_.StartPreview(cam_id_, preview_resolution, jpeg_resolution, |
| recording_resolution)); |
| |
| Do3AConverged(); |
| |
| // Get face region |
| cam_service_.StartFaceDetection(cam_id_); |
| ASSERT_EQ(0, cam_service_.WaitForPreviewFrames(cam_id_, kNumPreviewFrames, |
| kTimeoutMsPerFrame)); |
| ASSERT_NE(nullptr, preview_result_metadata_.get()) |
| << "Result metadata is unavailable"; |
| camera_metadata_ro_entry_t entry; |
| int result = |
| find_camera_metadata_ro_entry(preview_result_metadata_.get(), |
| ANDROID_STATISTICS_FACE_RECTANGLES, &entry); |
| ASSERT_EQ(0, result) |
| << "Metadata key ANDROID_STATISTICS_FACE_RECTANGLES not found"; |
| if (expected_num_faces_ * 4 != entry.count) { |
| if (!dump_path_.empty()) { |
| // dump image for debugging |
| const camera_metadata_t* metadata = |
| cam_service_.ConstructDefaultRequestSettings( |
| cam_id_, CAMERA3_TEMPLATE_STILL_CAPTURE); |
| cam_service_.TakeStillCapture(cam_id_, metadata); |
| |
| struct timespec timeout; |
| clock_gettime(CLOCK_REALTIME, &timeout); |
| timeout.tv_sec += 1; |
| ASSERT_EQ(0, WaitStillCaptureResult(cam_id_, timeout)) |
| << "Waiting for still capture result timeout"; |
| |
| ScopedImageI420 image = |
| ConvertJpegToI420(still_capture_results_[cam_id_].buffer_handles[0], |
| jpeg_resolution.Width(), jpeg_resolution.Height()); |
| if (image != nullptr) { |
| image->SaveToFile(dump_path_); |
| } |
| } |
| ASSERT_TRUE(false) << "Expect face rectangles size " |
| << expected_num_faces_ * 4 << " but detected " |
| << entry.count; |
| } |
| |
| int32_t x1 = entry.data.i32[0]; |
| int32_t y1 = entry.data.i32[1]; |
| int32_t x2 = entry.data.i32[2]; |
| int32_t y2 = entry.data.i32[3]; |
| LOGF(INFO) << "Face rectangle(x1, y1, x2, y2):" << x1 << " " << y1 << " " |
| << x2 << " " << y2; |
| |
| preview_result_metadata_.reset(); |
| |
| if (!enable_face_ae) { |
| cam_service_.StopFaceDetection(cam_id_); |
| ASSERT_EQ(0, cam_service_.WaitForPreviewFrames(cam_id_, kNumPreviewFrames, |
| kTimeoutMsPerFrame)); |
| } |
| |
| const camera_metadata_t* metadata = |
| cam_service_.ConstructDefaultRequestSettings( |
| cam_id_, CAMERA3_TEMPLATE_STILL_CAPTURE); |
| cam_service_.TakeStillCapture(cam_id_, metadata); |
| |
| struct timespec timeout; |
| clock_gettime(CLOCK_REALTIME, &timeout); |
| timeout.tv_sec += 1; |
| ASSERT_EQ(0, WaitStillCaptureResult(cam_id_, timeout)) |
| << "Waiting for still capture result timeout"; |
| cam_service_.StopPreview(cam_id_); |
| |
| ScopedImageI420 image = |
| ConvertJpegToI420(still_capture_results_[cam_id_].buffer_handles[0], |
| jpeg_resolution.Width(), jpeg_resolution.Height()); |
| ASSERT_NE(image, nullptr); |
| // Calculate average Y value. |
| int32_t total_y = 0; |
| int32_t total_points = 0; |
| for (size_t y = y1; y <= y2; ++y) { |
| for (size_t x = x1; x <= x2; ++x) { |
| total_y += image->y_addr[y * image->y_stride + x]; |
| ++total_points; |
| // Draw rectangle for debugging |
| if (y == y1 || y == y2 || x == x1 || x == x2) { |
| image->y_addr[y * image->y_stride + x] = 0; |
| } |
| } |
| } |
| if (!dump_path_.empty()) { |
| image->SaveToFile(dump_path_); |
| } |
| LOGF(INFO) << "Luma Value:" << total_y / total_points; |
| } |
| |
| TEST_P(Camera3FaceAutoExposureTest, GetFaceLumaValueWithFaceAutoExposure) { |
| // Run only if --expected_num_faces argument presented. |
| if (expected_num_faces_ < 0) { |
| GTEST_SKIP(); |
| } |
| GetFaceLumaValue(true); |
| } |
| |
| TEST_P(Camera3FaceAutoExposureTest, GetFaceLumaValueWithoutFaceAutoExposure) { |
| // Run only if --expected_num_faces argument presented. |
| if (expected_num_faces_ < 0) { |
| GTEST_SKIP(); |
| } |
| GetFaceLumaValue(false); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| Camera3FaceTest, |
| Camera3FaceAutoExposureTest, |
| ::testing::ValuesIn(Camera3Module().GetTestCameraIds())); |
| |
| } // namespace camera3_test |