1074 lines
40 KiB
C++
1074 lines
40 KiB
C++
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// Licensed under the MIT License.
|
|
|
|
//************************ Includes *****************************
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
#include <k4a/k4a.h>
|
|
#include <k4ainternal/common.h>
|
|
#include <k4ainternal/logging.h>
|
|
#include <gtest/gtest.h>
|
|
#include <utcommon.h>
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <time.h>
|
|
#include <k4a/k4a.h>
|
|
#include <azure_c_shared_utility/threadapi.h>
|
|
#include <azure_c_shared_utility/envvariable.h>
|
|
#include <deque>
|
|
#include <mutex>
|
|
|
|
#ifndef _WIN32
|
|
#include <time.h>
|
|
#endif
|
|
|
|
#define LLD(val) ((int64_t)(val))
|
|
#define STS_TO_MS(ts) (LLD((ts) / 1000000)) // System TS convertion to milliseconds
|
|
|
|
static bool g_skip_delay_off_color_validation = false;
|
|
static int32_t g_depth_delay_off_color_usec = 0;
|
|
static uint8_t g_device_index = K4A_DEVICE_DEFAULT;
|
|
static k4a_wired_sync_mode_t g_wired_sync_mode = K4A_WIRED_SYNC_MODE_STANDALONE;
|
|
static int g_capture_count = 50;
|
|
static bool g_synchronized_images_only = false;
|
|
static bool g_no_startup_flush = false;
|
|
static uint32_t g_subordinate_delay_off_master_usec = 0;
|
|
static bool g_manual_exposure = true;
|
|
static uint32_t g_exposure_setting = 31000; // will round up to nearest value
|
|
static bool g_power_line_50_hz = false;
|
|
|
|
using ::testing::ValuesIn;
|
|
|
|
typedef struct _sys_pts_time_t
|
|
{
|
|
uint64_t pts;
|
|
uint64_t system;
|
|
} sys_pts_time_t;
|
|
|
|
static std::mutex g_lock_mutex;
|
|
static std::deque<sys_pts_time_t> g_time_c; // Color image copy of data
|
|
static std::deque<sys_pts_time_t> g_time_i; // Ir image copy of data
|
|
|
|
struct latency_parameters
|
|
{
|
|
int test_number;
|
|
const char *test_name;
|
|
k4a_fps_t fps;
|
|
k4a_image_format_t color_format;
|
|
k4a_color_resolution_t color_resolution;
|
|
k4a_depth_mode_t depth_mode;
|
|
|
|
friend std::ostream &operator<<(std::ostream &os, const latency_parameters &obj)
|
|
{
|
|
return os << "test index: (" << obj.test_name << ") " << (int)obj.test_number;
|
|
}
|
|
};
|
|
|
|
struct thread_data
|
|
{
|
|
volatile bool save_samples;
|
|
volatile bool exit;
|
|
volatile uint32_t imu_samples;
|
|
k4a_device_t device;
|
|
};
|
|
|
|
class latency_perf : public ::testing::Test, public ::testing::WithParamInterface<latency_parameters>
|
|
{
|
|
public:
|
|
virtual void SetUp()
|
|
{
|
|
ASSERT_EQ(K4A_RESULT_SUCCEEDED, k4a_device_open(g_device_index, &m_device)) << "Couldn't open device\n";
|
|
ASSERT_NE(m_device, nullptr);
|
|
EXPECT_NE((FILE *)NULL, (m_file_handle = fopen("latency_testResults.csv", "a")));
|
|
}
|
|
|
|
virtual void TearDown()
|
|
{
|
|
if (m_device != nullptr)
|
|
{
|
|
k4a_device_close(m_device);
|
|
m_device = nullptr;
|
|
}
|
|
if (m_file_handle)
|
|
{
|
|
fclose(m_file_handle);
|
|
}
|
|
}
|
|
|
|
void print_and_log(const char *message, const char *mode, int64_t ave, int64_t min, int64_t max);
|
|
void process_image(k4a_capture_t capture,
|
|
uint64_t current_system_ts,
|
|
bool process_color,
|
|
bool *image_first_pass,
|
|
std::deque<uint64_t> *system_latency,
|
|
std::deque<uint64_t> *system_latency_from_pts,
|
|
uint64_t *system_ts_last,
|
|
uint64_t *system_ts_from_pts_last);
|
|
|
|
k4a_device_t m_device = nullptr;
|
|
FILE *m_file_handle;
|
|
};
|
|
|
|
static const char *get_string_from_color_format(k4a_image_format_t format)
|
|
{
|
|
switch (format)
|
|
{
|
|
case K4A_IMAGE_FORMAT_COLOR_NV12:
|
|
return "K4A_IMAGE_FORMAT_COLOR_NV12";
|
|
break;
|
|
case K4A_IMAGE_FORMAT_COLOR_YUY2:
|
|
return "K4A_IMAGE_FORMAT_COLOR_YUY2";
|
|
break;
|
|
case K4A_IMAGE_FORMAT_COLOR_MJPG:
|
|
return "K4A_IMAGE_FORMAT_COLOR_MJPG";
|
|
break;
|
|
case K4A_IMAGE_FORMAT_COLOR_BGRA32:
|
|
return "K4A_IMAGE_FORMAT_COLOR_BGRA32";
|
|
break;
|
|
case K4A_IMAGE_FORMAT_DEPTH16:
|
|
return "K4A_IMAGE_FORMAT_DEPTH16";
|
|
break;
|
|
case K4A_IMAGE_FORMAT_IR16:
|
|
return "K4A_IMAGE_FORMAT_IR16";
|
|
break;
|
|
case K4A_IMAGE_FORMAT_CUSTOM8:
|
|
return "K4A_IMAGE_FORMAT_CUSTOM8";
|
|
break;
|
|
case K4A_IMAGE_FORMAT_CUSTOM16:
|
|
return "K4A_IMAGE_FORMAT_CUSTOM16";
|
|
break;
|
|
case K4A_IMAGE_FORMAT_CUSTOM:
|
|
return "K4A_IMAGE_FORMAT_CUSTOM";
|
|
break;
|
|
}
|
|
assert(0);
|
|
return "K4A_IMAGE_FORMAT_UNKNOWN";
|
|
}
|
|
|
|
static const char *get_string_from_color_resolution(k4a_color_resolution_t resolution)
|
|
{
|
|
switch (resolution)
|
|
{
|
|
case K4A_COLOR_RESOLUTION_OFF:
|
|
return "OFF";
|
|
break;
|
|
case K4A_COLOR_RESOLUTION_720P:
|
|
return "1280 * 720 16:9";
|
|
break;
|
|
case K4A_COLOR_RESOLUTION_1080P:
|
|
return "1920 * 1080 16:9";
|
|
break;
|
|
case K4A_COLOR_RESOLUTION_1440P:
|
|
return "2560 * 1440 16:9";
|
|
break;
|
|
case K4A_COLOR_RESOLUTION_1536P:
|
|
return "2048 * 1536 4:3";
|
|
break;
|
|
case K4A_COLOR_RESOLUTION_2160P:
|
|
return "3840 * 2160 16:9";
|
|
break;
|
|
case K4A_COLOR_RESOLUTION_3072P:
|
|
return "4096 * 3072 4:3";
|
|
break;
|
|
}
|
|
assert(0);
|
|
return "Unknown resolution";
|
|
}
|
|
|
|
static const char *get_string_from_depth_mode(k4a_depth_mode_t mode)
|
|
{
|
|
switch (mode)
|
|
{
|
|
case K4A_DEPTH_MODE_OFF:
|
|
return "K4A_DEPTH_MODE_OFF";
|
|
break;
|
|
case K4A_DEPTH_MODE_NFOV_2X2BINNED:
|
|
return "K4A_DEPTH_MODE_NFOV_2X2BINNED";
|
|
break;
|
|
case K4A_DEPTH_MODE_NFOV_UNBINNED:
|
|
return "K4A_DEPTH_MODE_NFOV_UNBINNED";
|
|
break;
|
|
case K4A_DEPTH_MODE_WFOV_2X2BINNED:
|
|
return "K4A_DEPTH_MODE_WFOV_2X2BINNED";
|
|
break;
|
|
case K4A_DEPTH_MODE_WFOV_UNBINNED:
|
|
return "K4A_DEPTH_MODE_WFOV_UNBINNED";
|
|
break;
|
|
case K4A_DEPTH_MODE_PASSIVE_IR:
|
|
return "K4A_DEPTH_MODE_PASSIVE_IR";
|
|
break;
|
|
}
|
|
assert(0);
|
|
return "Unknown Depth";
|
|
}
|
|
|
|
static bool get_system_time(uint64_t *time_nsec)
|
|
{
|
|
k4a_result_t result = K4A_RESULT_SUCCEEDED;
|
|
#ifdef _WIN32
|
|
LARGE_INTEGER qpc = { 0 };
|
|
static LARGE_INTEGER freq = { 0 };
|
|
result = K4A_RESULT_FROM_BOOL(QueryPerformanceCounter(&qpc) != 0);
|
|
if (K4A_FAILED(result))
|
|
{
|
|
return false;
|
|
}
|
|
if (freq.QuadPart == 0)
|
|
{
|
|
result = K4A_RESULT_FROM_BOOL(QueryPerformanceFrequency(&freq) != 0);
|
|
if (K4A_FAILED(result))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Calculate seconds in such a way we minimize overflow.
|
|
// Rollover happens, for a 1MHz Freq, when qpc.QuadPart > 0x003F FFFF FFFF FFFF; ~571 Years after boot.
|
|
*time_nsec = qpc.QuadPart / freq.QuadPart * 1000000000;
|
|
*time_nsec += qpc.QuadPart % freq.QuadPart * 1000000000 / freq.QuadPart;
|
|
|
|
#else
|
|
struct timespec ts_time;
|
|
result = K4A_RESULT_FROM_BOOL(clock_gettime(CLOCK_MONOTONIC, &ts_time) == 0);
|
|
if (K4A_FAILED(result))
|
|
{
|
|
return false;
|
|
}
|
|
// Rollover happens about ~136 years after boot.
|
|
*time_nsec = (uint64_t)ts_time.tv_sec * 1000000000 + (uint64_t)ts_time.tv_nsec;
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
static int _latency_imu_thread(void *param)
|
|
{
|
|
struct thread_data *data = (struct thread_data *)param;
|
|
k4a_result_t result;
|
|
k4a_imu_sample_t imu;
|
|
|
|
result = k4a_device_start_imu(data->device);
|
|
if (K4A_FAILED(result))
|
|
{
|
|
printf("Failed to start imu\n");
|
|
return result;
|
|
}
|
|
|
|
g_time_c.clear();
|
|
g_time_i.clear();
|
|
|
|
while (data->exit == false)
|
|
{
|
|
k4a_wait_result_t wresult = k4a_device_get_imu_sample(data->device, &imu, 10);
|
|
if (wresult == K4A_WAIT_RESULT_FAILED)
|
|
{
|
|
printf("k4a_device_get_imu_sample failed\n");
|
|
result = K4A_RESULT_FAILED;
|
|
break;
|
|
}
|
|
else if ((wresult == K4A_WAIT_RESULT_SUCCEEDED) && (data->save_samples))
|
|
{
|
|
sys_pts_time_t time;
|
|
time.pts = imu.acc_timestamp_usec;
|
|
if (get_system_time(&time.system) == 0)
|
|
{
|
|
result = K4A_RESULT_FAILED;
|
|
break;
|
|
}
|
|
|
|
// Save data to each of the queues
|
|
g_lock_mutex.lock();
|
|
g_time_c.push_back(time);
|
|
g_time_i.push_back(time);
|
|
g_lock_mutex.unlock();
|
|
}
|
|
};
|
|
|
|
k4a_device_stop_imu(data->device);
|
|
return result;
|
|
}
|
|
|
|
// Drop the lock and sleep for Xms. This is to allow the queue to fill again. Return if we yield too long.
|
|
#define YIELD_THREAD(lock_var, count, message) \
|
|
lock_var.unlock(); \
|
|
printf("Lock dropped while %s\n", message); \
|
|
ThreadAPI_Sleep(2); \
|
|
if (++count > 15) \
|
|
{ \
|
|
EXPECT_LT(count, 15); \
|
|
return 0; \
|
|
} \
|
|
lock_var.lock();
|
|
|
|
static uint64_t lookup_system_ts(uint64_t pts_ts, bool color)
|
|
{
|
|
sys_pts_time_t last_time;
|
|
uint64_t start_time_nsec;
|
|
uint64_t current_time_nsec;
|
|
int count = 0;
|
|
|
|
bool found = false;
|
|
|
|
std::deque<sys_pts_time_t> *time_queue = &g_time_i;
|
|
if (color)
|
|
{
|
|
time_queue = &g_time_c;
|
|
}
|
|
|
|
g_lock_mutex.lock();
|
|
|
|
// Record start time
|
|
if (get_system_time(&start_time_nsec) == 0)
|
|
{
|
|
printf("ERROR getting system time\n");
|
|
EXPECT_TRUE(0);
|
|
g_lock_mutex.unlock();
|
|
return 0;
|
|
}
|
|
|
|
int delay_count = 0;
|
|
while (time_queue->empty())
|
|
{
|
|
// Drop lock, wait, retake lock - Exit if taking too long
|
|
YIELD_THREAD(g_lock_mutex, delay_count, "Initializing")
|
|
}
|
|
|
|
last_time = time_queue->front();
|
|
time_queue->pop_front();
|
|
|
|
while (!found)
|
|
{
|
|
int x;
|
|
for (x = 0; !time_queue->empty(); x++)
|
|
{
|
|
last_time = time_queue->front();
|
|
if (pts_ts > last_time.pts)
|
|
{
|
|
// Hold onto last_time for 1 more loop
|
|
last_time = time_queue->front();
|
|
time_queue->pop_front();
|
|
}
|
|
else
|
|
{
|
|
// We just found the first system time that is beyond the one we are looking for.
|
|
if ((pts_ts - last_time.pts) < (time_queue->front().pts - pts_ts))
|
|
{
|
|
g_lock_mutex.unlock();
|
|
found = true;
|
|
return last_time.system;
|
|
}
|
|
uint64_t ret_time = time_queue->front().system;
|
|
g_lock_mutex.unlock();
|
|
|
|
found = true;
|
|
return ret_time;
|
|
}
|
|
|
|
if (get_system_time(¤t_time_nsec) == 0)
|
|
{
|
|
printf("ERROR getting system time\n");
|
|
EXPECT_TRUE(0);
|
|
g_lock_mutex.unlock();
|
|
return 0;
|
|
}
|
|
|
|
if (STS_TO_MS(current_time_nsec - start_time_nsec) > 1000)
|
|
{
|
|
printf("Count for break is %d\n", count);
|
|
break; // Don't hold lock too long, run YIELD_THREAD below
|
|
}
|
|
}
|
|
|
|
// Queue is drained or we held the lock too long. We need to let the IMU thread catch up. Drop lock, wait,
|
|
// retake lock - Exit if taking too long
|
|
YIELD_THREAD(g_lock_mutex, delay_count, "walking list.");
|
|
|
|
// Update start time after the thread yield
|
|
if (get_system_time(&start_time_nsec) == 0)
|
|
{
|
|
printf("ERROR getting system time\n");
|
|
EXPECT_TRUE(0);
|
|
g_lock_mutex.unlock();
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Should not happen
|
|
EXPECT_FALSE(1);
|
|
g_lock_mutex.unlock();
|
|
return 0;
|
|
}
|
|
|
|
void latency_perf::print_and_log(const char *message, const char *mode, int64_t ave, int64_t min, int64_t max)
|
|
{
|
|
printf(" %30s %30s: Ave=%" PRId64 " min=%" PRId64 " max=%" PRId64 "\n", message, mode, ave, min, max);
|
|
|
|
if (m_file_handle)
|
|
{
|
|
char buffer[1024];
|
|
snprintf(buffer,
|
|
sizeof(buffer),
|
|
"%s, %s (min ave max),%" PRId64 ",%" PRId64 ",%" PRId64 ",",
|
|
mode,
|
|
message,
|
|
min,
|
|
ave,
|
|
max);
|
|
fputs(buffer, m_file_handle);
|
|
}
|
|
}
|
|
|
|
void latency_perf::process_image(k4a_capture_t capture,
|
|
uint64_t current_system_ts,
|
|
bool process_color,
|
|
bool *image_first_pass,
|
|
std::deque<uint64_t> *system_latency,
|
|
std::deque<uint64_t> *system_latency_from_pts,
|
|
uint64_t *system_ts_last,
|
|
uint64_t *system_ts_from_pts_last)
|
|
{
|
|
k4a_image_t image;
|
|
if (process_color)
|
|
{
|
|
image = k4a_capture_get_color_image(capture);
|
|
}
|
|
else
|
|
{
|
|
image = k4a_capture_get_ir_image(capture);
|
|
}
|
|
|
|
if (image)
|
|
{
|
|
uint64_t system_ts = k4a_image_get_system_timestamp_nsec(image);
|
|
|
|
uint64_t system_ts_from_pts = lookup_system_ts(k4a_image_get_device_timestamp_usec(image), process_color);
|
|
|
|
// Time from center of exposure until given to us from the SDK; based on Host system time.
|
|
uint64_t system_ts_latency = current_system_ts - system_ts;
|
|
|
|
// Time from center of exposure PTS time (converted to system time based on low latency IMU data) until we
|
|
// read the frame; based on Host system time.
|
|
uint64_t system_ts_latency_from_pts = current_system_ts - system_ts_from_pts;
|
|
if (system_ts_from_pts > current_system_ts)
|
|
{
|
|
printf("Calculated %s pts system time %" PRId64 " is after our arrival system time %" PRId64
|
|
" a diff of %" PRId64 "\n",
|
|
process_color ? "color" : "IR",
|
|
STS_TO_MS(system_ts_from_pts),
|
|
STS_TO_MS(current_system_ts),
|
|
STS_TO_MS(system_ts_from_pts - current_system_ts));
|
|
|
|
// Update values anyway
|
|
*system_ts_last = system_ts;
|
|
*system_ts_from_pts_last = system_ts_from_pts;
|
|
}
|
|
else
|
|
{
|
|
|
|
if (!*image_first_pass)
|
|
{
|
|
system_latency->push_back(current_system_ts - system_ts);
|
|
system_latency_from_pts->push_back(system_ts_latency_from_pts);
|
|
|
|
printf("| %9" PRId64 " [%5" PRId64 "] [%5" PRId64 "] ",
|
|
STS_TO_MS(system_ts),
|
|
STS_TO_MS(system_ts_latency),
|
|
STS_TO_MS(system_ts_latency_from_pts));
|
|
|
|
// TS should increase
|
|
EXPECT_GT(system_ts, *system_ts_last);
|
|
EXPECT_GT(system_ts_from_pts, *system_ts_from_pts_last);
|
|
}
|
|
*system_ts_last = system_ts;
|
|
*system_ts_from_pts_last = system_ts_from_pts;
|
|
*image_first_pass = false;
|
|
}
|
|
|
|
k4a_image_release(image);
|
|
}
|
|
else
|
|
{
|
|
printf("| ");
|
|
}
|
|
}
|
|
|
|
TEST_P(latency_perf, testTest)
|
|
{
|
|
auto as = GetParam();
|
|
const int32_t TIMEOUT_IN_MS = 1000;
|
|
k4a_capture_t capture = NULL;
|
|
int capture_count = g_capture_count;
|
|
bool failed = false;
|
|
k4a_device_configuration_t config = K4A_DEVICE_CONFIG_INIT_DISABLE_ALL;
|
|
thread_data thread = { 0 };
|
|
THREAD_HANDLE th1 = NULL;
|
|
std::deque<uint64_t> color_system_latency;
|
|
std::deque<uint64_t> color_system_latency_from_pts;
|
|
std::deque<uint64_t> ir_system_latency;
|
|
std::deque<uint64_t> ir_system_latency_from_pts;
|
|
uint64_t current_system_ts = 0;
|
|
uint64_t color_system_ts_last = 0, color_system_ts_from_pts_last = 0;
|
|
uint64_t ir_system_ts_last = 0, ir_system_ts_from_pts_last = 0;
|
|
int32_t read_exposure = 0;
|
|
|
|
printf("Capturing %d frames for test: %s\n", g_capture_count, as.test_name);
|
|
|
|
{
|
|
int32_t power_line_setting = g_power_line_50_hz ? 1 : 2;
|
|
ASSERT_EQ(K4A_RESULT_SUCCEEDED,
|
|
k4a_device_set_color_control(m_device,
|
|
K4A_COLOR_CONTROL_POWERLINE_FREQUENCY,
|
|
K4A_COLOR_CONTROL_MODE_MANUAL,
|
|
power_line_setting));
|
|
printf("Power line mode set to manual and %s.\n", power_line_setting == 1 ? "50Hz" : "60Hz");
|
|
}
|
|
|
|
if (g_manual_exposure)
|
|
{
|
|
k4a_color_control_mode_t read_mode;
|
|
ASSERT_EQ(K4A_RESULT_SUCCEEDED,
|
|
k4a_device_set_color_control(m_device,
|
|
K4A_COLOR_CONTROL_EXPOSURE_TIME_ABSOLUTE,
|
|
K4A_COLOR_CONTROL_MODE_MANUAL,
|
|
(int32_t)g_exposure_setting));
|
|
ASSERT_EQ(K4A_RESULT_SUCCEEDED,
|
|
k4a_device_get_color_control(m_device,
|
|
K4A_COLOR_CONTROL_EXPOSURE_TIME_ABSOLUTE,
|
|
&read_mode,
|
|
&read_exposure));
|
|
printf(
|
|
"Setting exposure to manual mode, exposure target is: %d. Actual mode is: %s. Actual value is: %d.\n",
|
|
g_exposure_setting,
|
|
read_mode == K4A_COLOR_CONTROL_MODE_AUTO ? "auto" : "manual",
|
|
read_exposure);
|
|
read_exposure = 0; // Clear this so we read it again after sensor is started.
|
|
}
|
|
else
|
|
{
|
|
ASSERT_EQ(K4A_RESULT_SUCCEEDED,
|
|
k4a_device_set_color_control(m_device,
|
|
K4A_COLOR_CONTROL_EXPOSURE_TIME_ABSOLUTE,
|
|
K4A_COLOR_CONTROL_MODE_AUTO,
|
|
0));
|
|
printf("Auto Exposure\n");
|
|
read_exposure = 0;
|
|
}
|
|
|
|
config.color_format = as.color_format;
|
|
config.color_resolution = as.color_resolution;
|
|
config.depth_mode = as.depth_mode;
|
|
config.camera_fps = as.fps;
|
|
config.depth_delay_off_color_usec = g_depth_delay_off_color_usec;
|
|
config.wired_sync_mode = g_wired_sync_mode;
|
|
config.synchronized_images_only = g_synchronized_images_only;
|
|
config.subordinate_delay_off_master_usec = g_subordinate_delay_off_master_usec;
|
|
|
|
printf("Config being used is:\n");
|
|
printf(" color_format:%d\n", config.color_format);
|
|
printf(" color_resolution:%d\n", config.color_resolution);
|
|
printf(" depth_mode:%d\n", config.depth_mode);
|
|
printf(" camera_fps:%d\n", config.camera_fps);
|
|
printf(" synchronized_images_only:%d\n", config.synchronized_images_only);
|
|
printf(" depth_delay_off_color_usec:%d\n", config.depth_delay_off_color_usec);
|
|
printf(" wired_sync_mode:%d\n", config.wired_sync_mode);
|
|
printf(" subordinate_delay_off_master_usec:%d\n", config.subordinate_delay_off_master_usec);
|
|
printf(" disable_streaming_indicator:%d\n", config.disable_streaming_indicator);
|
|
printf("\n");
|
|
ASSERT_EQ(K4A_RESULT_SUCCEEDED, k4a_device_start_cameras(m_device, &config));
|
|
|
|
thread.device = m_device;
|
|
ASSERT_EQ(THREADAPI_OK, ThreadAPI_Create(&th1, _latency_imu_thread, &thread));
|
|
|
|
if (!g_no_startup_flush)
|
|
{
|
|
//
|
|
// Wait for streams to start and then purge the data collected
|
|
//
|
|
if (as.fps == K4A_FRAMES_PER_SECOND_30)
|
|
{
|
|
printf("Flushing first 2s of data\n");
|
|
ThreadAPI_Sleep(2000);
|
|
}
|
|
else if (as.fps == K4A_FRAMES_PER_SECOND_15)
|
|
{
|
|
printf("Flushing first 3s of data\n");
|
|
ThreadAPI_Sleep(3000);
|
|
}
|
|
else
|
|
{
|
|
printf("Flushing first 4s of data\n");
|
|
ThreadAPI_Sleep(4000);
|
|
}
|
|
while (K4A_WAIT_RESULT_SUCCEEDED == k4a_device_get_capture(m_device, &capture, 0))
|
|
{
|
|
// Drain the queue
|
|
k4a_capture_release(capture);
|
|
};
|
|
}
|
|
else
|
|
{
|
|
printf("Flushing no start of stream data\n");
|
|
}
|
|
|
|
// For consistent IMU timing, block entering the while loop until we get 1 sample
|
|
if (K4A_WAIT_RESULT_SUCCEEDED == k4a_device_get_capture(m_device, &capture, 1000))
|
|
{
|
|
k4a_capture_release(capture);
|
|
capture = NULL;
|
|
}
|
|
|
|
printf("Sys lat: is this difference in the system time recorded on the image and the system time when the image "
|
|
"was presented to the caller.\n");
|
|
printf(
|
|
"PTS lat: Similar to Sys lat, but instead of using the system time assigned to the image (which is recorded by "
|
|
"the Host PC), the image PTS (which is center of exposure in single camera mode) is used to "
|
|
"calculate a more accurate system time from when the same PTS arrived from the least latent sensor source, "
|
|
"IMU. The IMU data received is turned into a list of PTS values and associated system ts's for when each "
|
|
"sample arrived on system.\n");
|
|
printf("+---------------------------+---------------------------+\n");
|
|
printf("| Color Info (ms) | IR 16 Info (ms) |\n");
|
|
printf("| system [ sys ] [ PTS ] | system [ sys ] [ PTS ] |\n");
|
|
printf("| ts [ lat ] [ lat ] | ts [ lat ] [ lat ] |\n");
|
|
printf("+---------------------------+---------------------------+\n");
|
|
|
|
thread.save_samples = true; // start saving IMU samples
|
|
bool color_first_pass = true;
|
|
bool ir_first_pass = true;
|
|
capture_count++; // to account for dropping the first sample
|
|
while (capture_count-- > 0)
|
|
{
|
|
if (capture)
|
|
{
|
|
k4a_capture_release(capture);
|
|
}
|
|
|
|
// Get a depth frame
|
|
k4a_wait_result_t wresult = k4a_device_get_capture(m_device, &capture, TIMEOUT_IN_MS);
|
|
if (wresult != K4A_WAIT_RESULT_SUCCEEDED)
|
|
{
|
|
if (wresult == K4A_WAIT_RESULT_TIMEOUT)
|
|
{
|
|
printf("Timed out waiting for a capture\n");
|
|
}
|
|
else // wresult == K4A_WAIT_RESULT_FAILED:
|
|
{
|
|
printf("Failed to read a capture\n");
|
|
capture_count = 0;
|
|
}
|
|
failed = true;
|
|
continue;
|
|
}
|
|
|
|
if (get_system_time(¤t_system_ts) == 0)
|
|
{
|
|
printf("Timed out waiting for a capture\n");
|
|
failed = true;
|
|
continue;
|
|
}
|
|
|
|
if (read_exposure == 0)
|
|
{
|
|
k4a_image_t image = k4a_capture_get_color_image(capture);
|
|
if (image)
|
|
{
|
|
read_exposure = (int32_t)k4a_image_get_exposure_usec(image);
|
|
k4a_image_release(image);
|
|
}
|
|
}
|
|
|
|
process_image(capture,
|
|
current_system_ts,
|
|
true, // Color Image
|
|
&color_first_pass,
|
|
&color_system_latency,
|
|
&color_system_latency_from_pts,
|
|
&color_system_ts_last,
|
|
&color_system_ts_from_pts_last);
|
|
process_image(capture,
|
|
current_system_ts,
|
|
false, // IR Image
|
|
&ir_first_pass,
|
|
&ir_system_latency,
|
|
&ir_system_latency_from_pts,
|
|
&ir_system_ts_last,
|
|
&ir_system_ts_from_pts_last);
|
|
|
|
printf("|\n"); // End of line
|
|
} // End capture loop
|
|
|
|
thread.exit = true; // shut down IMU thread
|
|
k4a_device_stop_cameras(m_device);
|
|
if (capture)
|
|
{
|
|
k4a_capture_release(capture);
|
|
}
|
|
|
|
int thread_result;
|
|
ASSERT_EQ(THREADAPI_OK, ThreadAPI_Join(th1, &thread_result));
|
|
ASSERT_EQ(thread_result, (int)K4A_RESULT_SUCCEEDED);
|
|
|
|
printf("\nLatency Results:\n");
|
|
|
|
{
|
|
// init CSV line
|
|
if (m_file_handle != 0)
|
|
{
|
|
std::time_t date_time = std::time(NULL);
|
|
char buffer_date_time[100];
|
|
std::strftime(buffer_date_time, sizeof(buffer_date_time), "%c", localtime(&date_time));
|
|
|
|
const char *computer_name = environment_get_variable("COMPUTERNAME");
|
|
const char *disable_synchronization = environment_get_variable("K4A_DISABLE_SYNCHRONIZATION");
|
|
|
|
char buffer[1024];
|
|
snprintf(buffer,
|
|
sizeof(buffer),
|
|
"%s, %s, %s, %s,%s, %s, fps, %d, %s, captures, %d, %d, %d,",
|
|
buffer_date_time,
|
|
computer_name ? computer_name : "computer name not set",
|
|
as.test_name,
|
|
disable_synchronization ? disable_synchronization : "0",
|
|
get_string_from_color_format(as.color_format),
|
|
get_string_from_color_resolution(as.color_resolution),
|
|
k4a_convert_fps_to_uint(as.fps),
|
|
get_string_from_depth_mode(as.depth_mode),
|
|
g_capture_count,
|
|
g_manual_exposure,
|
|
read_exposure);
|
|
fputs(buffer, m_file_handle);
|
|
}
|
|
}
|
|
{
|
|
uint64_t color_system_latency_ave = 0;
|
|
uint64_t min = (uint64_t)-1;
|
|
uint64_t max = 0;
|
|
for (size_t x = 0; x < color_system_latency.size(); x++)
|
|
{
|
|
color_system_latency_ave += color_system_latency[x];
|
|
if (color_system_latency[x] < min)
|
|
{
|
|
min = color_system_latency[x];
|
|
}
|
|
if (color_system_latency[x] > max)
|
|
{
|
|
max = color_system_latency[x];
|
|
}
|
|
}
|
|
color_system_latency_ave = color_system_latency_ave / color_system_latency.size();
|
|
print_and_log("Color System Time Latency",
|
|
get_string_from_color_format(config.color_format),
|
|
STS_TO_MS(color_system_latency_ave),
|
|
STS_TO_MS(min),
|
|
STS_TO_MS(max));
|
|
}
|
|
{
|
|
uint64_t color_system_latency_from_pts_ave = 0;
|
|
uint64_t min = (uint64_t)-1;
|
|
uint64_t max = 0;
|
|
for (size_t x = 0; x < color_system_latency_from_pts.size(); x++)
|
|
{
|
|
color_system_latency_from_pts_ave += color_system_latency_from_pts[x];
|
|
if (color_system_latency_from_pts[x] < min)
|
|
{
|
|
min = color_system_latency_from_pts[x];
|
|
}
|
|
if (color_system_latency_from_pts[x] > max)
|
|
{
|
|
max = color_system_latency_from_pts[x];
|
|
}
|
|
}
|
|
color_system_latency_from_pts_ave = color_system_latency_from_pts_ave / color_system_latency_from_pts.size();
|
|
print_and_log("Color System Time PTS Latency",
|
|
get_string_from_color_format(config.color_format),
|
|
STS_TO_MS(color_system_latency_from_pts_ave),
|
|
STS_TO_MS(min),
|
|
STS_TO_MS(max));
|
|
}
|
|
{
|
|
uint64_t ir_system_latency_ave = 0;
|
|
uint64_t min = (uint64_t)-1;
|
|
uint64_t max = 0;
|
|
for (size_t x = 0; x < ir_system_latency.size(); x++)
|
|
{
|
|
ir_system_latency_ave += ir_system_latency[x];
|
|
if (ir_system_latency[x] < min)
|
|
{
|
|
min = ir_system_latency[x];
|
|
}
|
|
if (ir_system_latency[x] > max)
|
|
{
|
|
max = ir_system_latency[x];
|
|
}
|
|
}
|
|
ir_system_latency_ave = ir_system_latency_ave / ir_system_latency.size();
|
|
print_and_log(" IR System Time Latency",
|
|
get_string_from_depth_mode(config.depth_mode),
|
|
STS_TO_MS(ir_system_latency_ave),
|
|
STS_TO_MS(min),
|
|
STS_TO_MS(max));
|
|
}
|
|
{
|
|
uint64_t ir_system_latency_from_pts_ave = 0;
|
|
uint64_t min = (uint64_t)-1;
|
|
uint64_t max = 0;
|
|
for (size_t x = 0; x < ir_system_latency_from_pts.size(); x++)
|
|
{
|
|
ir_system_latency_from_pts_ave += ir_system_latency_from_pts[x];
|
|
if (ir_system_latency_from_pts[x] < min)
|
|
{
|
|
min = ir_system_latency_from_pts[x];
|
|
}
|
|
if (ir_system_latency_from_pts[x] > max)
|
|
{
|
|
max = ir_system_latency_from_pts[x];
|
|
}
|
|
}
|
|
ir_system_latency_from_pts_ave = ir_system_latency_from_pts_ave / ir_system_latency_from_pts.size();
|
|
print_and_log(" IR System Time PTS",
|
|
get_string_from_depth_mode(config.depth_mode),
|
|
STS_TO_MS(ir_system_latency_from_pts_ave),
|
|
STS_TO_MS(min),
|
|
STS_TO_MS(max));
|
|
}
|
|
|
|
printf("\n");
|
|
if (m_file_handle != 0)
|
|
{
|
|
// Terminate line
|
|
fputs("\n", m_file_handle);
|
|
}
|
|
|
|
ASSERT_EQ(K4A_RESULT_SUCCEEDED,
|
|
k4a_device_set_color_control(m_device,
|
|
K4A_COLOR_CONTROL_EXPOSURE_TIME_ABSOLUTE,
|
|
K4A_COLOR_CONTROL_MODE_AUTO,
|
|
0));
|
|
|
|
ASSERT_EQ(failed, false);
|
|
return;
|
|
}
|
|
|
|
// K4A_DEPTH_MODE_WFOV_UNBINNED is the most demanding depth mode, only runs at 15FPS or less
|
|
|
|
// clang-format off
|
|
// PASSIVE_IR is fastest Depth Mode - YUY2 is fastest Color mode
|
|
static struct latency_parameters tests_30fps[] = {
|
|
// All Color modes with fast Depth
|
|
{ 0, "FPS_30_MJPEG_2160P_PASSIVE_IR", K4A_FRAMES_PER_SECOND_30, K4A_IMAGE_FORMAT_COLOR_MJPG, K4A_COLOR_RESOLUTION_2160P, K4A_DEPTH_MODE_PASSIVE_IR},
|
|
{ 1, "FPS_30_MJPEG_1536P_PASSIVE_IR", K4A_FRAMES_PER_SECOND_30, K4A_IMAGE_FORMAT_COLOR_MJPG, K4A_COLOR_RESOLUTION_1536P, K4A_DEPTH_MODE_PASSIVE_IR},
|
|
{ 2, "FPS_30_MJPEG_1440P_PASSIVE_IR", K4A_FRAMES_PER_SECOND_30, K4A_IMAGE_FORMAT_COLOR_MJPG, K4A_COLOR_RESOLUTION_1440P, K4A_DEPTH_MODE_PASSIVE_IR},
|
|
{ 3, "FPS_30_MJPEG_1080P_PASSIVE_IR", K4A_FRAMES_PER_SECOND_30, K4A_IMAGE_FORMAT_COLOR_MJPG, K4A_COLOR_RESOLUTION_1080P, K4A_DEPTH_MODE_PASSIVE_IR},
|
|
{ 4, "FPS_30_MJPEG_0720P_PASSIVE_IR", K4A_FRAMES_PER_SECOND_30, K4A_IMAGE_FORMAT_COLOR_MJPG, K4A_COLOR_RESOLUTION_720P, K4A_DEPTH_MODE_PASSIVE_IR},
|
|
{ 5, "FPS_30_NV12__0720P_PASSIVE_IR", K4A_FRAMES_PER_SECOND_30, K4A_IMAGE_FORMAT_COLOR_NV12, K4A_COLOR_RESOLUTION_720P, K4A_DEPTH_MODE_PASSIVE_IR},
|
|
{ 6, "FPS_30_YUY2__0720P_PASSIVE_IR", K4A_FRAMES_PER_SECOND_30, K4A_IMAGE_FORMAT_COLOR_YUY2, K4A_COLOR_RESOLUTION_720P, K4A_DEPTH_MODE_PASSIVE_IR},
|
|
{ 7, "FPS_30_BGRA32_2160P_PASSIVE_IR", K4A_FRAMES_PER_SECOND_30, K4A_IMAGE_FORMAT_COLOR_BGRA32, K4A_COLOR_RESOLUTION_2160P, K4A_DEPTH_MODE_PASSIVE_IR},
|
|
{ 8, "FPS_30_BGRA32_1536P_PASSIVE_IR", K4A_FRAMES_PER_SECOND_30, K4A_IMAGE_FORMAT_COLOR_BGRA32, K4A_COLOR_RESOLUTION_1536P, K4A_DEPTH_MODE_PASSIVE_IR},
|
|
{ 9, "FPS_30_BGRA32_1440P_PASSIVE_IR", K4A_FRAMES_PER_SECOND_30, K4A_IMAGE_FORMAT_COLOR_BGRA32, K4A_COLOR_RESOLUTION_1440P, K4A_DEPTH_MODE_PASSIVE_IR},
|
|
{ 10, "FPS_30_BGRA32_1080P_PASSIVE_IR", K4A_FRAMES_PER_SECOND_30, K4A_IMAGE_FORMAT_COLOR_BGRA32, K4A_COLOR_RESOLUTION_1080P, K4A_DEPTH_MODE_PASSIVE_IR},
|
|
{ 11, "FPS_30_BGRA32_0720P_PASSIVE_IR", K4A_FRAMES_PER_SECOND_30, K4A_IMAGE_FORMAT_COLOR_BGRA32, K4A_COLOR_RESOLUTION_720P, K4A_DEPTH_MODE_PASSIVE_IR},
|
|
|
|
// All Depth Modes with fastest Color
|
|
{ 12, "FPS_30_YUY2__0720P_NFOV_2X2BINNED", K4A_FRAMES_PER_SECOND_30, K4A_IMAGE_FORMAT_COLOR_YUY2, K4A_COLOR_RESOLUTION_720P, K4A_DEPTH_MODE_NFOV_2X2BINNED},
|
|
{ 13, "FPS_30_YUY2__0720P_NFOV_UNBINNED", K4A_FRAMES_PER_SECOND_30, K4A_IMAGE_FORMAT_COLOR_YUY2, K4A_COLOR_RESOLUTION_720P, K4A_DEPTH_MODE_NFOV_UNBINNED},
|
|
{ 14, "FPS_30_YUY2__0720P_WFOV_2X2BINNED", K4A_FRAMES_PER_SECOND_30, K4A_IMAGE_FORMAT_COLOR_YUY2, K4A_COLOR_RESOLUTION_720P, K4A_DEPTH_MODE_WFOV_2X2BINNED},
|
|
{ 15, "FPS_30_YUY2__0720P_PASSIVE_IR", K4A_FRAMES_PER_SECOND_30, K4A_IMAGE_FORMAT_COLOR_YUY2, K4A_COLOR_RESOLUTION_720P, K4A_DEPTH_MODE_PASSIVE_IR},
|
|
|
|
};
|
|
|
|
INSTANTIATE_TEST_CASE_P(30FPS_TESTS, latency_perf, ValuesIn(tests_30fps));
|
|
|
|
static struct latency_parameters tests_15fps[] = {
|
|
// All Color modes with fast Depth
|
|
{ 0, "FPS_15_MJPEG_3072P_PASSIVE_IR", K4A_FRAMES_PER_SECOND_15, K4A_IMAGE_FORMAT_COLOR_MJPG, K4A_COLOR_RESOLUTION_3072P, K4A_DEPTH_MODE_PASSIVE_IR},
|
|
{ 1, "FPS_15_BGRA32_3072P_PASSIVE_IR", K4A_FRAMES_PER_SECOND_15, K4A_IMAGE_FORMAT_COLOR_BGRA32, K4A_COLOR_RESOLUTION_3072P, K4A_DEPTH_MODE_PASSIVE_IR},
|
|
|
|
// All Depth Modes with fastest Color
|
|
{ 2, "FPS_15_YUY2__0720P_WFOV_UNBINNED", K4A_FRAMES_PER_SECOND_15, K4A_IMAGE_FORMAT_COLOR_YUY2, K4A_COLOR_RESOLUTION_720P, K4A_DEPTH_MODE_WFOV_UNBINNED},
|
|
};
|
|
|
|
INSTANTIATE_TEST_CASE_P(15FPS_TESTS, latency_perf, ValuesIn(tests_15fps));
|
|
// clang-format on
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
bool error = false;
|
|
k4a_unittest_init();
|
|
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
|
|
for (int i = 1; i < argc; ++i)
|
|
{
|
|
char *argument = argv[i];
|
|
for (int j = 0; argument[j]; j++)
|
|
{
|
|
argument[j] = (char)tolower(argument[j]);
|
|
}
|
|
if (strcmp(argument, "--depth_delay_off_color") == 0)
|
|
{
|
|
if (i + 1 <= argc)
|
|
{
|
|
|
|
g_depth_delay_off_color_usec = (int32_t)strtol(argv[i + 1], NULL, 10);
|
|
printf("Setting g_depth_delay_off_color_usec = %d\n", g_depth_delay_off_color_usec);
|
|
i++;
|
|
}
|
|
else
|
|
{
|
|
printf("Error: depth_delay_off_color parameter missing\n");
|
|
error = true;
|
|
}
|
|
}
|
|
else if (strcmp(argument, "--skip_delay_off_color_validation") == 0)
|
|
{
|
|
g_skip_delay_off_color_validation = true;
|
|
}
|
|
else if (strcmp(argument, "--master") == 0)
|
|
{
|
|
g_wired_sync_mode = K4A_WIRED_SYNC_MODE_MASTER;
|
|
printf("Setting g_wired_sync_mode = K4A_WIRED_SYNC_MODE_MASTER\n");
|
|
}
|
|
else if (strcmp(argument, "--subordinate") == 0)
|
|
{
|
|
g_wired_sync_mode = K4A_WIRED_SYNC_MODE_SUBORDINATE;
|
|
printf("Setting g_wired_sync_mode = K4A_WIRED_SYNC_MODE_SUBORDINATE\n");
|
|
}
|
|
else if (strcmp(argument, "--synchronized_images_only") == 0)
|
|
{
|
|
g_synchronized_images_only = true;
|
|
printf("g_synchronized_images_only = true\n");
|
|
}
|
|
else if (strcmp(argument, "--no_startup_flush") == 0)
|
|
{
|
|
g_no_startup_flush = true;
|
|
printf("g_no_startup_flush = true\n");
|
|
}
|
|
else if (strcmp(argument, "--60hz") == 0)
|
|
{
|
|
g_power_line_50_hz = false;
|
|
printf("g_power_line_50_hz = false\n");
|
|
}
|
|
else if (strcmp(argument, "--50hz") == 0)
|
|
{
|
|
g_power_line_50_hz = true;
|
|
printf("g_power_line_50_hz = true\n");
|
|
}
|
|
else if (strcmp(argument, "--index") == 0)
|
|
{
|
|
if (i + 1 <= argc)
|
|
{
|
|
g_device_index = (uint8_t)strtol(argv[i + 1], NULL, 10);
|
|
printf("setting g_device_index = %d\n", g_device_index);
|
|
i++;
|
|
}
|
|
else
|
|
{
|
|
printf("Error: index parameter missing\n");
|
|
error = true;
|
|
}
|
|
}
|
|
else if (strcmp(argument, "--subordinate_delay_off_master_usec") == 0)
|
|
{
|
|
if (i + 1 <= argc)
|
|
{
|
|
g_subordinate_delay_off_master_usec = (uint32_t)strtol(argv[i + 1], NULL, 10);
|
|
printf("g_subordinate_delay_off_master_usec = %d\n", g_subordinate_delay_off_master_usec);
|
|
i++;
|
|
}
|
|
else
|
|
{
|
|
printf("Error: index parameter missing\n");
|
|
error = true;
|
|
}
|
|
}
|
|
else if (strcmp(argument, "--capture_count") == 0)
|
|
{
|
|
if (i + 1 <= argc)
|
|
{
|
|
g_capture_count = (int)strtol(argv[i + 1], NULL, 10);
|
|
printf("g_capture_count g_device_index = %d\n", g_capture_count);
|
|
i++;
|
|
}
|
|
else
|
|
{
|
|
printf("Error: index parameter missing\n");
|
|
error = true;
|
|
}
|
|
}
|
|
else if (strcmp(argument, "--exposure") == 0)
|
|
{
|
|
if (i + 1 <= argc)
|
|
{
|
|
g_exposure_setting = (uint32_t)strtol(argv[i + 1], NULL, 10);
|
|
printf("g_exposure_setting = %d\n", g_exposure_setting);
|
|
g_manual_exposure = true;
|
|
i++;
|
|
}
|
|
else
|
|
{
|
|
printf("Error: index parameter missing\n");
|
|
error = true;
|
|
}
|
|
}
|
|
else if (strcmp(argument, "--auto") == 0)
|
|
{
|
|
g_manual_exposure = false;
|
|
printf("Auto Exposure Enabled\n");
|
|
}
|
|
|
|
if ((strcmp(argument, "-h") == 0) || (strcmp(argument, "/h") == 0) || (strcmp(argument, "-?") == 0) ||
|
|
(strcmp(argument, "/?") == 0))
|
|
{
|
|
error = true;
|
|
}
|
|
}
|
|
|
|
if (error)
|
|
{
|
|
printf("\n\nOptional Custom Test Settings:\n");
|
|
printf(" --depth_delay_off_color <+/- microseconds>\n");
|
|
printf(" This is the time delay the depth image capture is delayed off the color.\n");
|
|
printf(" valid ranges for this are -1 frame time to +1 frame time. The percentage\n");
|
|
printf(" needs to be multiplied by 100 to achieve correct behavior; 10000 is \n");
|
|
printf(" 100.00%%, 100 is 1.00%%.\n");
|
|
printf(" --skip_delay_off_color_validation\n");
|
|
printf(" Set this when don't want the results of color to depth timestamp \n"
|
|
" measurements to allow your test run to fail. They will still be logged\n"
|
|
" to output and the CSV file.\n");
|
|
printf(" --master\n");
|
|
printf(" Run device in master mode\n");
|
|
printf(" --subordinate\n");
|
|
printf(" Run device in subordinate mode\n");
|
|
printf(" --index\n");
|
|
printf(" The device index to target when calling k4a_device_open()\n");
|
|
printf(" --capture_count\n");
|
|
printf(" The number of captures the test should read; default is 100\n");
|
|
printf(" --synchronized_images_only\n");
|
|
printf(" By default this setting is false, enabling this will for the test to wait for\n");
|
|
printf(" both and depth images to be available.\n");
|
|
printf(" --subordinate_delay_off_master_usec <+ microseconds>\n");
|
|
printf(" This is the time delay the device captures off the master devices capture sync\n");
|
|
printf(" pulse. This value needs to be less than one image sample period, i.e for 30FPS \n");
|
|
printf(" this needs to be less than 33333us.\n");
|
|
printf(" --no_startup_flush\n");
|
|
printf(" By default the test will wait for streams to run for X seconds to stabilize. This\n");
|
|
printf(" disables that.\n");
|
|
printf(" --exposure <exposure in usec>\n");
|
|
printf(" Deault is manual exposure with an exposure of 33,333us. This will test with the manual exposure "
|
|
"setting\n");
|
|
printf(" that is passed in.\n");
|
|
printf(" --auto\n");
|
|
printf(" By default the test uses manual exposure. This will test with auto exposure.\n");
|
|
printf(" --60hz\n");
|
|
printf(" <default> Sets the power line compensation frequency to 60Hz\n");
|
|
printf(" --50hz\n");
|
|
printf(" Sets the power line compensation frequency to 50Hz\n");
|
|
|
|
return 1; // Indicates an error or warning
|
|
}
|
|
int results = RUN_ALL_TESTS();
|
|
k4a_unittest_deinit();
|
|
return results;
|
|
}
|