From d29c91989753b9fabaeb0ddc3ed634c935e55ba7 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Tue, 23 May 2023 17:25:27 +0100 Subject: [PATCH 001/306] deleting mnt folder --- python/decode_raw10.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/python/decode_raw10.py b/python/decode_raw10.py index 7f6a126e..9f88b7ba 100644 --- a/python/decode_raw10.py +++ b/python/decode_raw10.py @@ -6,20 +6,17 @@ BGGR is the order of the Bayer pattern few padding bytes on the end of every row to match bits """ -import matplotlib.pyplot as plt import cv2 +import matplotlib.pyplot as plt import numpy as np -from PIL import Image # just to avoid color BGR issues when writting -from utils import * +from PIL import Image # just to avoid color BGR issues when writting +from dotenv import load_dotenv +load_dotenv() # take environment variables from .env. -image_name = "img_raw10.bin" -path = "/home/albertoisorna/xalbertoisorna/rawtreatment/newimgs/" -path='/mnt/c/Users/albertoisorna/exec/' +from utils import * -image_name = "img_640_480_10_0008.raw" -path = "/home/albertoisorna/xalbertoisorna/rawtreatment/images_binning/adquisition_2_raw/" +input_name = os.getenv('BINARY_IMG_PATH') -input_name = path+image_name width = 640 height = 480 bit_width = 10 From b6a82c8bef77650c9be0d8a1e22cc7894e610e9b Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 23 May 2023 17:31:08 +0100 Subject: [PATCH 002/306] cmake cleanup, changing xmos_cmake_toolchain to origin --- CMakeLists.txt | 28 +-- camera/CMakeLists.txt | 25 +- examples/examples.cmake | 25 +- examples/simple_timing/simple_timing.cmake | 15 -- examples/take_picture/take_picture.cmake | 15 -- .../take_picture_dynamic.cmake | 18 -- .../take_picture_dynamic_crop.cmake | 18 -- .../take_picture_rgb/take_picture_rgb.cmake | 15 -- modules/CMakeLists.txt | 1 - sensors/CMakeLists.txt | 27 +- tests/CMakelists.txt | 34 --- tests/conftest.py | 104 -------- tests/{src/main.c => dummy} | 0 tests/src/test_return_0/test_return_0.c | 7 - tests/wscript | 234 ------------------ xmos_cmake_toolchain | 2 +- 16 files changed, 31 insertions(+), 537 deletions(-) delete mode 100644 tests/CMakelists.txt delete mode 100644 tests/conftest.py rename tests/{src/main.c => dummy} (100%) delete mode 100644 tests/src/test_return_0/test_return_0.c delete mode 100644 tests/wscript diff --git a/CMakeLists.txt b/CMakeLists.txt index 377a89d8..66330f6e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,3 @@ -# edit here -------------------------------- -set(PROJECT_NAME first_project) - -# do not edit ---------------------------------------------------------------- cmake_minimum_required(VERSION 3.21) ## Disable in-source build. @@ -10,39 +6,25 @@ if("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}") endif() ## Project declaration -project(PROJECT_NAME) +project(fwk_camera) ## Enable languages for project enable_language(CXX C ASM) -## Project options -option(XCORE_VOICE_TESTS "Enable XCORE-VOICE tests" OFF) - -## Import some helpful macros -include(xmos_cmake_toolchain/xmos_macros.cmake) - -## Setup a root path -set(first_project_PATH ${PROJECT_SOURCE_DIR} CACHE STRING "Root folder of sln_voice in this cmake project tree") - +if(NOT ${CMAKE_SYSTEM_PROCESSOR} STREQUAL XCORE_XS3A) + message(FATAL_ERROR "Only XCORE_XS3A build is supported!") +endif() ## Add frameworks -if(IS_DIRECTORY modules) - add_subdirectory(modules) - message("Adding modules/") -endif() +add_subdirectory(modules) ## Add sensors add_subdirectory(sensors) -message("Adding sensors/") ## Add cameras add_subdirectory(camera) -message("Adding camera/") - - ## Add top level project targets if(PROJECT_IS_TOP_LEVEL) include(examples/examples.cmake) endif() - diff --git a/camera/CMakeLists.txt b/camera/CMakeLists.txt index 8a874a45..cf18e07f 100644 --- a/camera/CMakeLists.txt +++ b/camera/CMakeLists.txt @@ -1,18 +1,13 @@ # ############################################################################## # CMake configuration stuff -cmake_minimum_required(VERSION 3.14) -cmake_policy(SET CMP0057 NEW) enable_language(C CXX ASM) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -string(REPLACE "-MD" "-MMD" CMAKE_DEPFILE_FLAGS_C ${CMAKE_DEPFILE_FLAGS_C}) - # ############################################################################## # Target name set(LIB_NAME lib_camera) -set(LIB_NAME_ALIAS lib_camera_general) # Source files file(GLOB_RECURSE SOURCES_C "*.c") @@ -22,16 +17,14 @@ file(GLOB_RECURSE SOURCES_ASM "*.S") add_library(${LIB_NAME} STATIC) -target_include_directories(${LIB_NAME} PUBLIC . api) - -target_sources(${LIB_NAME} PRIVATE ${SOURCES_C} ${SOURCES_XC} ${SOURCES_CPP} - $<$:${SOURCES_ASM}>) - - -# ############################################################################## -# Add and link the library -add_library(${LIB_NAME_ALIAS} INTERFACE) +target_include_directories(${LIB_NAME} PUBLIC .) -target_link_libraries(${LIB_NAME_ALIAS} INTERFACE ${LIB_NAME}) +target_sources(${LIB_NAME} + PRIVATE + ${SOURCES_C} + ${SOURCES_XC} + ${SOURCES_CPP} + ${SOURCES_ASM} +) -add_library(camera::lib_camera ALIAS ${LIB_NAME_ALIAS}) +add_library(camera::lib_camera ALIAS ${LIB_NAME}) diff --git a/examples/examples.cmake b/examples/examples.cmake index c4739911..7ca3c829 100644 --- a/examples/examples.cmake +++ b/examples/examples.cmake @@ -9,25 +9,6 @@ set(EXAMPLES ) # add all the examples that you need -if(${CMAKE_SYSTEM_NAME} STREQUAL XCORE_XS3A) - foreach(EXAMPLE ${EXAMPLES}) - include(${CMAKE_CURRENT_LIST_DIR}/${EXAMPLE}/${EXAMPLE}.cmake) - endforeach(EXAMPLE) -else() - # Get the "version" value from the JSON element - file(READ settings.json JSON_STRING) - string(JSON SDK_VERSION GET ${JSON_STRING} ${IDX} version) - - # Determine OS, set up install dir - if(${CMAKE_SYSTEM_NAME} STREQUAL Windows) - set(HOST_INSTALL_DIR "$ENV{USERPROFILE}\\.xmos\\bin") - else() - set(HOST_INSTALL_DIR "/opt/xmos/bin") - endif() - - add_subdirectory(modules/xscope_fileio/xscope_fileio/host) - add_subdirectory(../../) - install(TARGETS xscope_host_endpoint DESTINATION ${HOST_INSTALL_DIR}) -endif() - - +foreach(EXAMPLE ${EXAMPLES}) + include(${CMAKE_CURRENT_LIST_DIR}/${EXAMPLE}/${EXAMPLE}.cmake) +endforeach(EXAMPLE) diff --git a/examples/simple_timing/simple_timing.cmake b/examples/simple_timing/simple_timing.cmake index f4e65119..665634c1 100644 --- a/examples/simple_timing/simple_timing.cmake +++ b/examples/simple_timing/simple_timing.cmake @@ -60,18 +60,3 @@ target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) - - -#********************** -# Create run and debug targets -#********************** -create_run_target(${TARGET}) -create_debug_target(${TARGET}) -create_flash_app_target(${TARGET}) -create_install_target(${TARGET}) - -# then custom command line -add_custom_command(TARGET ${TARGET} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy - ${CMAKE_BINARY_DIR}/*.xe /mnt/c/Users/albertoisorna/exec/) - diff --git a/examples/take_picture/take_picture.cmake b/examples/take_picture/take_picture.cmake index be6c7a9b..560e551a 100644 --- a/examples/take_picture/take_picture.cmake +++ b/examples/take_picture/take_picture.cmake @@ -60,18 +60,3 @@ target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) - - -#********************** -# Create run and debug targets -#********************** -create_run_target(${TARGET}) -create_debug_target(${TARGET}) -create_flash_app_target(${TARGET}) -create_install_target(${TARGET}) - -# then custom command line -add_custom_command(TARGET ${TARGET} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy - ${CMAKE_BINARY_DIR}/*.xe /mnt/c/Users/albertoisorna/exec/) - diff --git a/examples/take_picture_dynamic/take_picture_dynamic.cmake b/examples/take_picture_dynamic/take_picture_dynamic.cmake index 80a9e3c5..ed3c50ab 100644 --- a/examples/take_picture_dynamic/take_picture_dynamic.cmake +++ b/examples/take_picture_dynamic/take_picture_dynamic.cmake @@ -4,7 +4,6 @@ # <--- Set the executable set(TARGET example_take_picture_dynamic) -set(DEST_FOLDER /mnt/c/Users/albertoisorna/exec/) # to post build command file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.*c) set(APP_INCLUDES @@ -62,20 +61,3 @@ target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) - - -#********************** -# Create run and debug targets -#********************** -create_run_target(${TARGET}) -create_debug_target(${TARGET}) -create_flash_app_target(${TARGET}) -create_install_target(${TARGET}) - -# then custom command line to copy .xe file to custom folder -if(EXISTS ${DEST_FOLDER}) - add_custom_command(TARGET ${TARGET} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy - ${CMAKE_BINARY_DIR}/*.xe ${DEST_FOLDER}) -endif() - diff --git a/examples/take_picture_dynamic_crop/take_picture_dynamic_crop.cmake b/examples/take_picture_dynamic_crop/take_picture_dynamic_crop.cmake index 23f308ce..8da2767d 100644 --- a/examples/take_picture_dynamic_crop/take_picture_dynamic_crop.cmake +++ b/examples/take_picture_dynamic_crop/take_picture_dynamic_crop.cmake @@ -4,7 +4,6 @@ # <--- Set the executable set(TARGET example_take_picture_crop) -set(DEST_FOLDER /mnt/c/Users/albertoisorna/exec/) # to post build command file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.*c) set(APP_INCLUDES @@ -62,20 +61,3 @@ target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) - - -#********************** -# Create run and debug targets -#********************** -create_run_target(${TARGET}) -create_debug_target(${TARGET}) -create_flash_app_target(${TARGET}) -create_install_target(${TARGET}) - -# then custom command line to copy .xe file to custom folder -if(EXISTS ${DEST_FOLDER}) - add_custom_command(TARGET ${TARGET} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy - ${CMAKE_BINARY_DIR}/*.xe ${DEST_FOLDER}) -endif() - diff --git a/examples/take_picture_rgb/take_picture_rgb.cmake b/examples/take_picture_rgb/take_picture_rgb.cmake index 6900f8cf..7e11b6d6 100644 --- a/examples/take_picture_rgb/take_picture_rgb.cmake +++ b/examples/take_picture_rgb/take_picture_rgb.cmake @@ -60,18 +60,3 @@ target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) - - -#********************** -# Create run and debug targets -#********************** -create_run_target(${TARGET}) -create_debug_target(${TARGET}) -create_flash_app_target(${TARGET}) -create_install_target(${TARGET}) - -# then custom command line -add_custom_command(TARGET ${TARGET} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy - ${CMAKE_BINARY_DIR}/*.xe /mnt/c/Users/albertoisorna/exec/) - diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index 0e5e53ec..33dff2fa 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -1,4 +1,3 @@ add_subdirectory(core) add_subdirectory(mipi) add_subdirectory(i2c) - diff --git a/sensors/CMakeLists.txt b/sensors/CMakeLists.txt index 7ba28292..76eced6c 100644 --- a/sensors/CMakeLists.txt +++ b/sensors/CMakeLists.txt @@ -1,14 +1,10 @@ # ############################################################################## # CMake configuration stuff -cmake_minimum_required(VERSION 3.14) -cmake_policy(SET CMP0057 NEW) enable_language(C CXX ASM) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -string(REPLACE "-MD" "-MMD" CMAKE_DEPFILE_FLAGS_C ${CMAKE_DEPFILE_FLAGS_C}) - # ############################################################################## # Target name set(LIB_NAME lib_imx) @@ -21,17 +17,20 @@ file(GLOB_RECURSE SOURCES_ASM "*.S") add_library(${LIB_NAME} STATIC) -target_include_directories(${LIB_NAME} PUBLIC . api) +target_include_directories(${LIB_NAME} + PUBLIC + . + api +) -target_sources(${LIB_NAME} PRIVATE ${SOURCES_C} ${SOURCES_XC} ${SOURCES_CPP} - $<$:${SOURCES_ASM}>) +target_sources(${LIB_NAME} + PRIVATE + ${SOURCES_C} + ${SOURCES_XC} + ${SOURCES_CPP} + ${SOURCES_ASM} +) target_link_libraries(${LIB_NAME} PUBLIC lib_i2c) -# ############################################################################## -# Add and link the library -add_library(lib_imx_general INTERFACE) - -target_link_libraries(lib_imx_general INTERFACE ${LIB_NAME}) - -add_library(sensors::lib_imx ALIAS lib_imx_general) +add_library(sensors::lib_imx ALIAS ${LIB_NAME}) diff --git a/tests/CMakelists.txt b/tests/CMakelists.txt deleted file mode 100644 index f599bce0..00000000 --- a/tests/CMakelists.txt +++ /dev/null @@ -1,34 +0,0 @@ -if( NOT ${Python3_FOUND} ) - message(FATAL_ERROR "Python3 not found for running . ") -endif() - -#copy conftest.py in the build directory since pytest_collect_file only collects tests from the directory tree where conftest.py is present -configure_file( conftest.py conftest.py COPYONLY ) - -add_executable(fwk_voice_ns_unit_tests) -file( GLOB_RECURSE SOURCES_C src/*.c ) -target_sources(fwk_voice_ns_unit_tests - PRIVATE - ${SOURCES_C} - ${UNITY_PATH}/extras/fixture/src/unity_fixture.c) -target_include_directories(fwk_voice_ns_unit_tests - PRIVATE - src) -target_link_libraries(fwk_voice_ns_unit_tests - PUBLIC - fwk_voice::ns - fwk_voice::test::shared::test_utils - fwk_voice::test::shared::unity) -if(${CMAKE_SYSTEM_NAME} STREQUAL XCORE_XS3A) - target_compile_options(fwk_voice_ns_unit_tests - PRIVATE - "-DUNITY_SUPPORT_64" - "-Wno-xcore-fptrgroup" - "-report") - - target_link_options(fwk_voice_ns_unit_tests - PRIVATE - "-target=${XCORE_TARGET}") -else() - target_link_libraries(fwk_voice_ns_unit_tests m) -endif() \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 91b85d75..00000000 --- a/tests/conftest.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright 2022 XMOS LIMITED. -# This Software is subject to the terms of the XMOS Public Licence: Version 1. -from builtins import str -import os.path -import pytest -import subprocess -import xtagctl - - -def pytest_collect_file(parent, path): - if(path.ext == ".xe"): - print('path = ',path) - return UnityTestSource.from_parent(parent, fspath=path) - -class UnityTestSource(pytest.File): - def collect(self): - # Find the binary built from the runner for this test file - # - # Assume the following directory layout: - # unit_tests/ <- Test root directory - # |-- bin/ <- Compiled binaries of the test runners - # |-- conftest.py <- This file - # |-- runners/ <- Auto-generated buildable source of test binaries - # |-- src/ <- Unity test functions - # `-- wscript <- Build system file used to generate/build runners - print("self.name ", self.fspath) - yield UnityTestExecutable.from_parent(self, fspath=self.fspath, name=self.name) - - -class UnityTestExecutable(pytest.Item): - def __init__(self, fspath, name, parent): - super(UnityTestExecutable, self).__init__(name, parent) - self.fspath = fspath - self._nodeid = self.name # Override the naming to suit C better - - def runtest(self): - # Run the binary in the simulator - simulator_fail = False - test_output = None - try: - print("run xrun for executable ", self.fspath) - with xtagctl.acquire("XCORE-AI-EXPLORER") as adapter_id: - test_output = subprocess.check_output(['xrun', '--xscope', '--io', '--adapter-id', adapter_id, self.fspath], text=True, stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as e: - # Unity exits non-zero if an assertion fails - simulator_fail = True - test_output = e.output - - # Parse the Unity output - unity_pass = False - test_output = test_output.split('\n') - for line in test_output: - if 'test' in line: - test_report = line.split(':') - # Unity output is as follows: - # :::PASS - # :::FAIL: - test_source = test_report[0] - line_number = test_report[1] - test_case = test_report[2] - result = test_report[3] - failure_reason = None - print(('\n {}()'.format(test_case)), end=' ') - if result == 'PASS': - unity_pass = True - continue - if result == 'FAIL': - failure_reason = test_report[4] - print('') # Insert line break after test_case print - raise UnityTestException(self, {'test_source': test_source, - 'line_number': line_number, - 'test_case': test_case, - 'failure_reason': - failure_reason}) - - if simulator_fail: - raise Exception(self, "Simulation failed.") - #if not unity_pass: - # raise Exception(self, "Unity test output not found.") - print('') # Insert line break after final test_case which passed - - def repr_failure(self, excinfo): - if isinstance(excinfo.value, UnityTestException): - return '\n'.join([str(self.parent).strip('<>'), - '{}:{}:{}()'.format( - excinfo.value[1]['test_source'], - excinfo.value[1]['line_number'], - excinfo.value[1]['test_case']), - 'Failure reason:', - excinfo.value[1]['failure_reason']]) - else: - return str(excinfo.value) - - def reportinfo(self): - # It's not possible to give sensible line number info for an executable - # so we return it as 0. - # - # The source line number will instead be recovered from the Unity print - # statements. - return self.fspath, 0, self.name - - -class UnityTestException(Exception): - pass \ No newline at end of file diff --git a/tests/src/main.c b/tests/dummy similarity index 100% rename from tests/src/main.c rename to tests/dummy diff --git a/tests/src/test_return_0/test_return_0.c b/tests/src/test_return_0/test_return_0.c deleted file mode 100644 index 71e3c64b..00000000 --- a/tests/src/test_return_0/test_return_0.c +++ /dev/null @@ -1,7 +0,0 @@ -#include "unity.h" - -void test_dummy_init(void) -{ - TEST_ASSERT_EQUAL_INT8( -1, 1); -} - diff --git a/tests/wscript b/tests/wscript deleted file mode 100644 index 83b27da9..00000000 --- a/tests/wscript +++ /dev/null @@ -1,234 +0,0 @@ -from __future__ import print_function -import glob -import os.path -import subprocess -import sys -from waflib import Options -from waflib.Build import BuildContext, CleanContext - - -def options(opt): - opt.add_option('--target', action='store', default='xcoreai') - opt.load('xwaf.xcommon') - -TARGETS = ['xcoreai'] - -def get_ruby(): - """ - Check ruby is avaliable and return the command to invoke it. - """ - interpreter_name = 'ruby' - try: - dev_null = open(os.devnull, 'w') - # Call the version command to check the interpreter can be run - subprocess.check_call([interpreter_name, '--version'], - stdout=dev_null, - close_fds=True) - except OSError as e: - print("Failed to run Ruby interpreter: {}".format(e), file=sys.stderr) - exit(1) # TODO: Check this is the correct way to kill xwaf on error - - return interpreter_name - - -def get_unity_runner_generator(project_root_path): - """ - Check the Unity generate_test_runner script is avaliable, and return the - path to it. - """ - unity_runner_generator = os.path.join( - project_root_path, 'Unity', 'auto', 'generate_test_runner.rb') - if not os.path.exists(unity_runner_generator): - print("Unity repo not found in workspace", file=sys.stderr) - exit(1) # TODO: Check this is the correct way to kill xwaf on error - return unity_runner_generator - - -def get_test_name(test_path): - """ - Return the test name by removing the extension from the filename. - """ - return os.path.splitext(os.path.basename(test_path))[0] - - -def get_file_type(filename): - """ - Return the extension from the filename. - """ - return filename.rsplit('.')[-1:][0] - - -def generate_unity_runner(project_root_path, unity_test_path, unity_runner_dir, - unity_runner_suffix): - """ - Invoke the Unity runner generation script for the given test file, and - return the path to the generated file. The output directory will be created - if it does not already exist. - """ - runner_path = os.path.join(os.path.join(unity_runner_dir, get_test_name(unity_test_path))) - if not os.path.exists(runner_path): - os.makedirs(runner_path) - - unity_runner_path = os.path.join( - runner_path, get_test_name(unity_test_path) + unity_runner_suffix - + '.' + 'c') - - try: - subprocess.check_call([get_ruby(), - get_unity_runner_generator(project_root_path), - unity_test_path, - unity_runner_path]) - except OSError as e: - print("Ruby generator failed for {}\n\t{}".format(unity_test_path, e), - file=sys.stderr) - exit(1) # TODO: Check this is the correct way to kill xwaf on error - - -def add_unity_runner_build_config(waf_conf, project_root_path, unity_test_path, - unity_runner_build_flags, target): - """ - Add a config to xwaf to build each Unity test runner into an xCORE - executable. - """ - waf_conf.setenv(get_test_name(unity_test_path) + '_' + target) - waf_conf.load('xwaf.compiler_xcc') - waf_conf.env.XCC_FLAGS = unity_runner_build_flags - waf_conf.env.PROJECT_ROOT = project_root_path - # TODO: can the xwaf boilerplate help here? - - -def prepare_unity_test_for_build(waf_conf, project_root_path, unity_test_path, - unity_runner_dir, unity_runner_suffix, target): - generate_unity_runner(project_root_path, unity_test_path, - unity_runner_dir, unity_runner_suffix) - runner_build_flags = '' # Could extract flags from the test name - add_unity_runner_build_config(waf_conf, project_root_path, unity_test_path, - runner_build_flags, target) - - -def find_unity_test_paths(unity_test_dir, unity_test_prefix): - """ - Return a list of all file paths with the unity_test_prefix found in the - unity_test_dir. - """ - file_list = [] - for root, dirs, files in os.walk(unity_test_dir): - for f in files: - if f.startswith(unity_test_prefix): - file_list.append(os.path.join(root,f)) - return file_list - -def find_unity_tests(unity_test_dir, unity_test_prefix): - """ - Return a dictionary of all {test names, test language} pairs with the - unity_test_prefix found in the unity_test_dir. - """ - unity_test_paths = find_unity_test_paths(unity_test_dir, unity_test_prefix) - return {get_test_name(path): get_file_type(path) - for path in unity_test_paths} - - -def generate_all_unity_runners(waf_conf, project_root_path, - unity_test_dir, unity_test_prefix, - unity_runner_dir, unity_runner_suffix): - """ - Generate a runner and a build config for each test file in the - unity_test_dir. - """ - # FIXME: pass unity_tests in? - unity_test_paths = find_unity_test_paths(unity_test_dir, unity_test_prefix) - for trgt in TARGETS: - for unity_test_path in unity_test_paths: - prepare_unity_test_for_build(waf_conf, project_root_path, - unity_test_path, - unity_runner_dir, unity_runner_suffix, trgt) - - -# TODO: can the xwaf boilerplate help here? -def create_waf_contexts(configs): - for trgt in TARGETS: - for test_name, test_language in configs.items(): - for ctx in (BuildContext, CleanContext): - raw_context = ctx.__name__.replace('Context', '').lower() - - class tmp(ctx): - cmd = raw_context + '_' + test_name + '_' + trgt - variant = test_name + '_' + trgt - #cmd = raw_context + '_' + test_name - #variant = test_name - language = test_language - target = trgt - runner = test_name - - -UNITY_TEST_DIR = 'src' -UNITY_TEST_PREFIX = 'test_' -UNITY_RUNNER_DIR = 'runners' -UNITY_RUNNER_SUFFIX = '_Runner' -UNITY_TESTS = find_unity_tests(UNITY_TEST_DIR, UNITY_TEST_PREFIX) - -create_waf_contexts(UNITY_TESTS) - -def configure(conf): - # TODO: move the call to generate_all_unity_runners() to build() - project_root = os.path.join('..', '..', '..') - generate_all_unity_runners(conf, project_root, - UNITY_TEST_DIR, UNITY_TEST_PREFIX, - UNITY_RUNNER_DIR, UNITY_RUNNER_SUFFIX) - conf.load('xwaf.xcommon') - -def build(bld): - if not bld.variant: - print('Adding test runners to build queue') - trgt = [ - c for c in TARGETS if c == bld.options.target - ] - - if len(trgt) == 0: - bld.fatal('specify a target with --target.\nAvailable targets: {}'.format(', '.join(TARGETS))) - return - - for name in UNITY_TESTS: - Options.commands.insert(0, 'build_' + name + '_' + trgt[0]) - #Options.commands.insert(0, 'build_' + name) - print('Build queue {}'.format(Options.commands)) - else: - print('Building runner {}'.format(bld.variant)) - subprocess.call( - ['../../.venv/bin/python', '../../lib_audio_pipelines/generate_config.py'] - ) - bld.env.XSCOPE = bld.path.find_resource('config.xscope') - - depends_on = ['lib_audio_pipelines', 'Unity'] - makefile_opts = {} - makefile_opts['SOURCE_DIRS'] = [os.path.join('src',bld.runner), os.path.join('runners',bld.runner)] - makefile_opts['XCC_FLAGS'] = ['-O2', '-g', '-Wall', '-report', '-DUNITY_SUPPORT_64', '-DDISABLE_TIMERAFTER_CHECKS_IN_AXE'] - if(bld.target == 'xcoreai'): - print('TARGET XCOREAI') - makefile_opts['TARGET'] = ['XCORE-AI-EXPLORER'] - makefile_opts['XCC_FLAGS'].append('-DVTB_USE_XS3_VPU=1') - - makefile_opts['INCLUDE_DIRS'] = ['src', 'autogenerated', '../../lib_audio_pipelines/api', '../../lib_audio_pipelines/src', '../../lib_audio_pipelines/config'] - makefile_opts['APP_NAME'] = [bld.variant] - makefile_opts['USED_MODULES'] = depends_on - makefile_opts['XCOMMON_MAKEFILE'] = ['Makefile.common'] - bld.do_xcommon(makefile_opts) - - -def test(bld): - # Call pytest to run Unity tests inside axe or xsim - try: - test_output = subprocess.check_output(['pytest']) - except subprocess.CalledProcessError as e: - # pytest exits non-zero if an assertion fails - test_output = e.output - print(test_output) - - -# TODO: ensure clean deletes the runners dir/ - -def dist(ctx): - ctx.load('xwaf.xcommon') - -def distcheck(ctx): - ctx.load('xwaf.xcommon') \ No newline at end of file diff --git a/xmos_cmake_toolchain b/xmos_cmake_toolchain index 166cc638..e577fbcf 160000 --- a/xmos_cmake_toolchain +++ b/xmos_cmake_toolchain @@ -1 +1 @@ -Subproject commit 166cc6389005cb6cc3539f8766399ff81366842f +Subproject commit e577fbcf147346264dbe25f82c5cc0e46e648cd6 From b1cda975a80deae11416723f4678062eeb144d8a Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Wed, 24 May 2023 10:28:56 +0100 Subject: [PATCH 003/306] decode raw 10 color correction --- python/decode_raw10.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/python/decode_raw10.py b/python/decode_raw10.py index 9f88b7ba..9b7d6084 100644 --- a/python/decode_raw10.py +++ b/python/decode_raw10.py @@ -56,15 +56,8 @@ img = demosaic(img, cfa_pattern, output_channel_order='RGB', alg_type='VNG') img_demoisaic = img -# color space transformation -color_matrix_1 = [0.9762914777, -0.2504389584, -0.1018426344, - -0.1751390547, 0.9807397723, 0.1705771685, - 0.04482413828, 0.1344814152, 0.4878755212] - -#img = old_apply_color_space_transform(img, color_matrix_1) - -# color transform 2 -# img = old_transform_xyz_to_srgb(img) +# color transforms +img = new_color_correction(img) # gamma img = img ** (1.0 / 2.2) From 144f4bb9137ad6841e8ae2071a11f330aa5b92f7 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Wed, 24 May 2023 10:57:40 +0100 Subject: [PATCH 004/306] removing cmake error --- CMakeLists.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 66330f6e..b2bdf609 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,9 +11,7 @@ project(fwk_camera) ## Enable languages for project enable_language(CXX C ASM) -if(NOT ${CMAKE_SYSTEM_PROCESSOR} STREQUAL XCORE_XS3A) - message(FATAL_ERROR "Only XCORE_XS3A build is supported!") -endif() +if(${CMAKE_SYSTEM_PROCESSOR} STREQUAL XCORE_XS3A) ## Add frameworks add_subdirectory(modules) @@ -27,4 +25,6 @@ add_subdirectory(camera) ## Add top level project targets if(PROJECT_IS_TOP_LEVEL) include(examples/examples.cmake) -endif() +endif() # top level + +endif() # xs3a From 65003e143671c9f0a471f2a6aa0ac478993443ac Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Wed, 24 May 2023 11:31:41 +0100 Subject: [PATCH 005/306] changing decode raw8 --- python/decode_raw8.py | 8 +------- sensors/.gitkeep | 0 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 sensors/.gitkeep diff --git a/python/decode_raw8.py b/python/decode_raw8.py index a33ccec7..31ee0f10 100644 --- a/python/decode_raw8.py +++ b/python/decode_raw8.py @@ -67,20 +67,14 @@ # demosaic img = demosaic(img, cfa_pattern, output_channel_order='RGB', alg_type='VNG') img_demoisaic = img -# wb -#img = gray_world(img) # color transforms img = new_color_correction(img) - - - # gamma img = img ** (1.0 / 1.8) # clip the image img = np.clip(255*img, 0, 255).astype(np.uint8) - # hist equalization (optional) -# img = run_histogram_equalization(img) +# img = run_histogram_equalization(img) # resize bilinear (optional) kfactor = 1 img = cv2.resize(img, (width // kfactor, height // kfactor), interpolation=cv2.INTER_AREA) diff --git a/sensors/.gitkeep b/sensors/.gitkeep deleted file mode 100644 index e69de29b..00000000 From e0d6cb3555b0d7a1bd0cdce67e6e35c464924a25 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Wed, 24 May 2023 12:09:50 +0100 Subject: [PATCH 006/306] adding bining horiontal without vertical option --- sensors/imx219_reg.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sensors/imx219_reg.h b/sensors/imx219_reg.h index b3bc9e1c..927c8eeb 100644 --- a/sensors/imx219_reg.h +++ b/sensors/imx219_reg.h @@ -11,17 +11,18 @@ #define CSI_LANE_MODE_4_LANES 3 // BINNING -#define BINNING_MODE_REG 0x0174 +#define BINNING_MODE_REG 0x0174 // HORIZONTAL | VERTICAL #define BINNING_NONE 0x0000 #define BINNING_2X2 0x0101 #define BINNING_4X4 0x0202 +#define BINNING_2Sx2 0x0302 #define BINNING_2X2_AN 0x0303 #if ((CONFIG_MODE == MODE_1920_1080) || (CONFIG_MODE == MODE_WQSXGA_RAW8)) #define BINNING_MODE BINNING_NONE #define VTPXCK_DIV 0x05 #else - #define BINNING_MODE BINNING_2X2_AN + #define BINNING_MODE BINNING_2X2 #define VTPXCK_DIV 0x0A #endif From 7b6cb0dde920f921c71ea1c260fa53b0aa5596ef Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Wed, 24 May 2023 15:05:35 +0100 Subject: [PATCH 007/306] sensor redefinition and statistics typo --- camera/isp.c | 28 +++--- camera/isp.h | 2 +- camera/stadistics.h | 32 ------- camera/{stadistics.c => statistics.c} | 71 +++++++++------- camera/statistics.h | 32 +++++++ .../take_picture_dynamic/src/process_frame.c | 12 +-- .../src/process_frame.c | 8 +- sensors/api/sensor.h | 85 +++++++++---------- sensors/api/sensor_defs.h | 11 +++ 9 files changed, 147 insertions(+), 134 deletions(-) delete mode 100644 camera/stadistics.h rename camera/{stadistics.c => statistics.c} (60%) create mode 100644 camera/statistics.h create mode 100644 sensors/api/sensor_defs.h diff --git a/camera/isp.c b/camera/isp.c index b4643272..4d1d0fae 100644 --- a/camera/isp.c +++ b/camera/isp.c @@ -68,15 +68,15 @@ void isp_AWB_gray_world(const uint32_t buffsize, uint8_t *green, uint8_t *blue) { - // compute stadistics by channel - Stadistics st_red; - Stadistics st_green; - Stadistics st_blue; + // compute statistics by channel + Statistics st_red; + Statistics st_green; + Statistics st_blue; uint8_t step = 1; - Stadistics_compute_all(buffsize, step, red, &st_red); - Stadistics_compute_all(buffsize, step, red, &st_green); - Stadistics_compute_all(buffsize, step, red, &st_blue); + Statistics_compute_all(buffsize, step, red, &st_red); + Statistics_compute_all(buffsize, step, red, &st_green); + Statistics_compute_all(buffsize, step, red, &st_blue); // apply white balancing float alfa = (st_green.mean)/(st_red.mean); @@ -89,15 +89,15 @@ void isp_AWB_percentile(const uint32_t buffsize, uint8_t *green, uint8_t *blue) { - // compute stadistics by channel - Stadistics st_red; - Stadistics st_green; - Stadistics st_blue; + // compute statistics by channel + Statistics st_red; + Statistics st_green; + Statistics st_blue; uint8_t step = 1; - Stadistics_compute_all(buffsize, step, red, &st_red); - Stadistics_compute_all(buffsize, step, red, &st_green); - Stadistics_compute_all(buffsize, step, red, &st_blue); + Statistics_compute_all(buffsize, step, red, &st_red); + Statistics_compute_all(buffsize, step, red, &st_green); + Statistics_compute_all(buffsize, step, red, &st_blue); // apply white balancing float alfa = 255/(st_red.percentile); diff --git a/camera/isp.h b/camera/isp.h index 1d7c24fb..99c75a6e 100644 --- a/camera/isp.h +++ b/camera/isp.h @@ -5,7 +5,7 @@ #include // null #include -#include "stadistics.h" // for dynamic AWB +#include "statistics.h" // for dynamic AWB // ----------------- ISP SETTINGS ------------------------------- #define DEFAULT_ALFA 1.58682 // default red gain (not used for the moment) diff --git a/camera/stadistics.h b/camera/stadistics.h deleted file mode 100644 index 5686f031..00000000 --- a/camera/stadistics.h +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef STADISTICS_H -#define STADISTICS_H - -#include -#include // memset -#include // null -#include // free, alloc - -typedef struct Stadistics{ - float histogram[64]; - float skewness; - uint8_t mean; - uint8_t max; - uint8_t min; - uint8_t percentile; -} Stadistics; - -Stadistics *Stadistics_alloc(void); -Stadistics Stadistics_initialize(void); -void Stadistics_free(Stadistics *self); - -void Stadistics_compute_all(const uint32_t buffsize, const uint8_t step, uint8_t *buffer, Stadistics *stadistics); -void Stadistics_compute_histogram(const uint32_t buffsize, const uint8_t step, uint8_t *buffer, Stadistics *stadistics); -void Stadistics_compute_skewness(Stadistics *stadistics); -void Stadistics_compute_minmaxavg(Stadistics *stadistics); -void Stadistics_compute_percentile(Stadistics *stadistics); -uint16_t Stadistics_compute_variance(Stadistics *stadistics); - -// aux functions -static char get_rgb_color(uint32_t pos, uint16_t width); - -#endif \ No newline at end of file diff --git a/camera/stadistics.c b/camera/statistics.c similarity index 60% rename from camera/stadistics.c rename to camera/statistics.c index 38406da7..bb401d7a 100644 --- a/camera/stadistics.c +++ b/camera/statistics.c @@ -1,4 +1,4 @@ -#include "stadistics.h" +#include "statistics.h" #define BINS 64 // changing the number of bins leads to change in implementation #define PERCENTILE_VALUE 0.05 // value selected for the percentile distribution @@ -10,8 +10,8 @@ #define GREEN 1 #define BLUE 2 -Stadistics *Stadistics_alloc(void) { - Stadistics *point; +Statistics *Statistics_alloc(void) { + Statistics *point; point = malloc(sizeof(*point)); if (point == NULL) { return NULL; @@ -20,30 +20,30 @@ Stadistics *Stadistics_alloc(void) { return point; } -void Stadistics_free(Stadistics *self) { +void Statistics_free(Statistics *self) { free(self); } -Stadistics Stadistics_initialize(void) { - Stadistics s = {{0}}; +Statistics Statistics_initialize(void) { + Statistics s = {{0}}; return s; } -void Stadistics_compute_histogram(const uint32_t buffsize, const uint8_t step, uint8_t *buffer, Stadistics *stadistics) +void Statistics_compute_histogram(const uint32_t buffsize, const uint8_t step, uint8_t *buffer, Statistics *statistics) { // fill the histogram for (uint32_t i=0; i< buffsize; i = i + step){ - stadistics->histogram[(buffer[i] >> 2)] += 1; // because 255/4 = 64 + statistics->histogram[(buffer[i] >> 2)] += 1; // because 255/4 = 64 } // normalize float inv_factor = (float)step / (float)buffsize; for (uint8_t j=0; j < BINS; j++){ - stadistics->histogram[j] *= inv_factor; + statistics->histogram[j] *= inv_factor; } } -void Stadistics_compute_skewness(Stadistics *stadistics) +void Statistics_compute_skewness(Statistics *statistics) { const float zk_values[] = { -1.0,-0.907753,-0.821362,-0.740633,-0.665375,-0.595396,-0.530504,-0.470508,-0.415214,-0.364431, @@ -57,14 +57,14 @@ void Stadistics_compute_skewness(Stadistics *stadistics) float skew = 0.0; for (int k = 0; k < BINS; k++) { - float pzk = stadistics->histogram[k]; // we asssumed values are normalized + float pzk = statistics->histogram[k]; // we asssumed values are normalized skew += zk_values[k] * pzk; } - stadistics -> skewness = skew; + statistics -> skewness = skew; } -void Stadistics_compute_minmaxavg(Stadistics *stadistics){ +void Statistics_compute_minmaxavg(Statistics *statistics){ float temp_mean = 0; uint8_t temp_min = 0; @@ -73,13 +73,13 @@ void Stadistics_compute_minmaxavg(Stadistics *stadistics){ // mean for (uint8_t k = 1; k < BINS; k++) // k=0 does not contribute to mean value { - temp_mean += stadistics->histogram[k] * k; // assuming histogram is normalized + temp_mean += statistics->histogram[k] * k; // assuming histogram is normalized } // min for (uint8_t k = 0; k < BINS; k++) { - if (stadistics->histogram[k]){ + if (statistics->histogram[k]){ temp_min = k; break; } @@ -88,53 +88,53 @@ void Stadistics_compute_minmaxavg(Stadistics *stadistics){ // max for (uint8_t k = BINS -1; k > 0; k--) { - if (stadistics->histogram[k]){ + if (statistics->histogram[k]){ temp_max = k; break; } } // Values *4 to return to 0-255 - stadistics -> mean = (uint8_t) (temp_mean+0.5) << 2; // +0.5 to avoid ceiling - stadistics -> min = (uint8_t) temp_min << 2; - stadistics -> max = (uint8_t) temp_max << 2; + statistics -> mean = (uint8_t) (temp_mean+0.5) << 2; // +0.5 to avoid ceiling + statistics -> min = (uint8_t) temp_min << 2; + statistics -> max = (uint8_t) temp_max << 2; } -void Stadistics_compute_percentile(Stadistics *stadistics){ +void Statistics_compute_percentile(Statistics *statistics){ float sump = 0.0; uint8_t k = 0; // percentile for (k = BINS - 1; k > 0; k--) { - sump += stadistics->histogram[k]; + sump += statistics->histogram[k]; if (sump > PERCENTILE_VALUE){ // I assume histogram is normalized to 0-1 break; } } - stadistics -> percentile = (k << 2); // Values *4 to return to 0-255 + statistics -> percentile = (k << 2); // Values *4 to return to 0-255 } -uint16_t Stadistics_compute_variance(Stadistics *stadistics){ - uint8_t mean = stadistics->mean >> 2; // /4 to return to histogram range 0-64 +uint16_t Statistics_compute_variance(Statistics *statistics){ + uint8_t mean = statistics->mean >> 2; // /4 to return to histogram range 0-64 float diff = 0.0; double variance = 0; for (uint8_t k = 0; k < BINS; k++){ - if (stadistics->histogram[k]){ + if (statistics->histogram[k]){ diff = (k - mean); diff = diff*diff; - variance += stadistics->histogram[k]*diff; + variance += statistics->histogram[k]*diff; } } variance = (uint16_t)(variance * 16); // Values *16 to return to 0-255 return variance; } -void Stadistics_compute_all(const uint32_t buffsize, const uint8_t step, uint8_t *buffer, Stadistics *stadistics){ - Stadistics_compute_histogram(buffsize, step, buffer, stadistics); - Stadistics_compute_skewness(stadistics); - Stadistics_compute_minmaxavg(stadistics); - Stadistics_compute_percentile(stadistics); +void Statistics_compute_all(const uint32_t buffsize, const uint8_t step, uint8_t *buffer, Statistics *statistics){ + Statistics_compute_histogram(buffsize, step, buffer, statistics); + Statistics_compute_skewness(statistics); + Statistics_compute_minmaxavg(statistics); + Statistics_compute_percentile(statistics); } static char get_rgb_color(uint32_t pos, uint16_t width) { @@ -143,4 +143,13 @@ static char get_rgb_color(uint32_t pos, uint16_t width) { uint32_t y = pos / width; uint8_t index = ((y & 1) << 1) | (x & 1); return color_table[index]; +} + + +void Statistics_print_info(Statistics *st){ + printf("min:%d, max:%d, mean:%d, percentile:%d", + st->min, + st->max, + st->mean, + st->percentile); } \ No newline at end of file diff --git a/camera/statistics.h b/camera/statistics.h new file mode 100644 index 00000000..1239dc38 --- /dev/null +++ b/camera/statistics.h @@ -0,0 +1,32 @@ +#ifndef STAtISTICS_H +#define STAtISTICS_H + +#include +#include // memset +#include // null +#include // free, alloc + +typedef struct Statistics{ + float histogram[64]; + float skewness; + uint8_t mean; + uint8_t max; + uint8_t min; + uint8_t percentile; +} Statistics; + +Statistics *Statistics_alloc(void); +Statistics Statistics_initialize(void); +void Statistics_free(Statistics *self); + +void Statistics_compute_all(const uint32_t buffsize, const uint8_t step, uint8_t *buffer, Statistics *statistics); +void Statistics_compute_histogram(const uint32_t buffsize, const uint8_t step, uint8_t *buffer, Statistics *statistics); +void Statistics_compute_skewness(Statistics *statistics); +void Statistics_compute_minmaxavg(Statistics *statistics); +void Statistics_compute_percentile(Statistics *statistics); +uint16_t Statistics_compute_variance(Statistics *statistics); + +// aux functions +void Statistics_print_info(Statistics *statistics); + +#endif \ No newline at end of file diff --git a/examples/take_picture_dynamic/src/process_frame.c b/examples/take_picture_dynamic/src/process_frame.c index b6d82022..4b0a36b3 100644 --- a/examples/take_picture_dynamic/src/process_frame.c +++ b/examples/take_picture_dynamic/src/process_frame.c @@ -6,7 +6,7 @@ #include #include "process_frame.h" -#include "stadistics.h" // for skewness and +#include "statistics.h" // for skewness and #include "utils.h" // for measuring time #include "isp.h" // setting auto_exposure, AWB @@ -15,7 +15,7 @@ const uint32_t img_len = MIPI_LINE_WIDTH_BYTES*MIPI_IMAGE_HEIGHT_PIXELS; float new_exp = 35; -Stadistics st = {{0}}; +Statistics st = {{0}}; // Write image to disk. This is called by camera main () to do the work void write_image(uint8_t *image) @@ -44,13 +44,13 @@ void process_image(uint8_t *image, chanend_t c){ } static int print_msg = 0; - // compute stadistics - Stadistics_compute_all(img_len, STEP, image, (Stadistics *) &st); + // compute statistics + Statistics_compute_all(img_len, STEP, image, (Statistics *) &st); float sk = st.skewness; // print information - printf("min:%d, max:%d, mean:%d, percentile:%d, exposure:%f, skewness:%f\n", - st.min, st.max, st.mean, st.percentile, new_exp, sk); + Statistics_print_info(&st); + printf("exposure:%f, skewness:%f\n", new_exp, sk); // exit condition if (sk < AE_MARGIN && sk > -AE_MARGIN){ diff --git a/examples/take_picture_dynamic_crop/src/process_frame.c b/examples/take_picture_dynamic_crop/src/process_frame.c index 18b2b41e..a2a77ff9 100644 --- a/examples/take_picture_dynamic_crop/src/process_frame.c +++ b/examples/take_picture_dynamic_crop/src/process_frame.c @@ -6,7 +6,7 @@ #include #include "process_frame.h" -#include "stadistics.h" // for skewness and +#include "statistics.h" // for skewness and #include "utils.h" // for measuring time #include "isp.h" // setting auto_exposure, AWB @@ -23,7 +23,7 @@ // Global definitions const uint32_t img_len = IMG_W*IMG_H; float new_exp = 35; -Stadistics st = {{0}}; +Statistics st = {{0}}; // Write image to disk. This is called by camera main () to do the work void write_image(uint8_t *image) @@ -52,8 +52,8 @@ void process_image(uint8_t *image, chanend_t c){ } static int print_msg = 0; - // compute stadistics - Stadistics_compute_all(img_len, STEP, image, (Stadistics *) &st); + // compute statistics + Statistics_compute_all(img_len, STEP, image, (Statistics *) &st); float sk = st.skewness; // print information diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index 59726203..fbbbd24a 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -1,34 +1,25 @@ // Sensor.h settings needed for custom sensor configuration - -#pragma once +#ifndef SENSOR_H +#define SENSOR_H #define XSTR(x) STR(x) #define STR(x) #x -// ----- minimal comom definitions -#define ON 1 -#define OFF 0 +// -------------- Sensor abstraction layer. -------------- +#include "sensor_defs.h" -#define MODE_VGA_RAW10 0 // 640x480 -#define MODE_VGA_RAW8 1 // 640x480 -#define MODE_UXGA_RAW8 2 // 1640x1232 -#define MODE_WQSXGA_RAW8 3 // 3280x2464 -#define MODE_1920_1080 4 +// This is user defined +#define CONFIG_IMX219_SUPPORT ENABLED +#define CONFIG_GC2145_SUPPORT DISABLED +#define CROP_ENABLED DISABLED -#define MIPI_DT_RAW8 0x2A -#define MIPI_DT_RAW10 0x2B +#define CONFIG_MODE MODE_VGA_640x480 +#define CONFIG_MIPI_FORMAT MIPI_DT_RAW8 -// -------------- Sensor abstraction layer. -------------- -// This is user defined -#define CONFIG_IMX219_SUPPORT ON -#define CONFIG_GC2145_SUPPORT OFF -#define CONFIG_MODE MODE_VGA_RAW8 #define MIPI_PKT_BUFFER_COUNT 4 -#define CROP_ENABLED OFF // FPS settings -// allowed values: [FPS_13, FPS_24, FPS_30, FPS_53, FPS_76] -#define FPS_13 +#define FPS_13 // allowed values: [FPS_13, FPS_24, FPS_30, FPS_53, FPS_76] // Inlcude custom libraries #if CONFIG_IMX219_SUPPORT @@ -40,36 +31,48 @@ #endif -// Modes definition -#if (CONFIG_MODE == MODE_VGA_RAW10) - #define EXPECTED_FORMAT MIPI_DT_RAW10 // RAW 10 bit data identifier +// Modes configurations +#ifndef CONFIG_MODE + #error CONFIG_MODE has to be defined +#endif + +#if (CONFIG_MODE == MODE_VGA_640x480) #define MIPI_IMAGE_WIDTH_PIXELS 640 // csi2 packed (stride 800) #define MIPI_IMAGE_HEIGHT_PIXELS 480 -#elif (CONFIG_MODE == MODE_VGA_RAW8) - #define EXPECTED_FORMAT MIPI_DT_RAW8 // RAW 8 bit data identifier - #define MIPI_IMAGE_WIDTH_PIXELS 640 - #define MIPI_IMAGE_HEIGHT_PIXELS 480 - -#elif (CONFIG_MODE == MODE_UXGA_RAW8) - #define EXPECTED_FORMAT MIPI_DT_RAW8 // RAW 8 bit data identifier +#elif (CONFIG_MODE == MODE_UXGA_1640x1232) #define MIPI_IMAGE_WIDTH_PIXELS 1640 // csi2 packed (stride 800) #define MIPI_IMAGE_HEIGHT_PIXELS 1232 -#elif (CONFIG_MODE == MODE_WQSXGA_RAW8) - #define EXPECTED_FORMAT MIPI_DT_RAW8 // RAW 8 bit data identifier +#elif (CONFIG_MODE == MODE_WQSXGA_3280x2464) #define MIPI_IMAGE_WIDTH_PIXELS 3280 // csi2 packed (stride 800) #define MIPI_IMAGE_HEIGHT_PIXELS 2464 -#elif (CONFIG_MODE == MODE_1920_1080) - #define EXPECTED_FORMAT MIPI_DT_RAW8 // RAW 8 bit data identifier +#elif (CONFIG_MODE == MODE_FHD_1920x1080) #define MIPI_IMAGE_WIDTH_PIXELS 1920 // csi2 packed (stride 800) #define MIPI_IMAGE_HEIGHT_PIXELS 1080 +#else + #error Unknown configuration mode #endif +// Pixel format configurations +#ifndef CONFIG_MIPI_FORMAT + #error CONFIG_MIPI_FORMAT has to be specified +#else + #if CONFIG_MIPI_FORMAT == MIPI_DT_RAW10 + #define MIPI_IMAGE_WIDTH_BYTES (((MIPI_IMAGE_WIDTH_PIXELS) >> 2) * 5) // by 5/4 + + #elif CONFIG_MIPI_FORMAT == MIPI_DT_RAW8 + #define MIPI_IMAGE_WIDTH_BYTES MIPI_IMAGE_WIDTH_PIXELS // same size -// Cropping mode + #else + #error CONFIG_MIPI_FORMAT not supported + #endif +#endif + + +// Cropping configurations #if (CROP_ENABLED) #define CROP_WIDTH_PIXELS 320 #define CROP_HEIGHT_PIXELS 240 @@ -94,17 +97,6 @@ #define camera_start(x) imx219_stream_start(x) #define camera_configure(x) imx219_configure_mode(x) - -// ------------------------------------------------------------------------------- -#if EXPECTED_FORMAT == MIPI_DT_RAW10 - #define MIPI_IMAGE_WIDTH_BYTES (((MIPI_IMAGE_WIDTH_PIXELS) >> 2) * 5) // by 5/4 - -#elif EXPECTED_FORMAT == MIPI_DT_RAW8 - #define MIPI_IMAGE_WIDTH_BYTES MIPI_IMAGE_WIDTH_PIXELS // same size -#else - #error Unknown format specified -#endif - // Camera dependant (do not edit) #define MIPI_LINE_WIDTH_BYTES MIPI_IMAGE_WIDTH_BYTES #define MIPI_MAX_PKT_SIZE_BYTES ((MIPI_LINE_WIDTH_BYTES) + 4) @@ -119,3 +111,4 @@ #endif +#endif // sensor_H \ No newline at end of file diff --git a/sensors/api/sensor_defs.h b/sensors/api/sensor_defs.h new file mode 100644 index 00000000..7d3f8b86 --- /dev/null +++ b/sensors/api/sensor_defs.h @@ -0,0 +1,11 @@ +// ----- minimal comom definitions +#define DISABLED 0 +#define ENABLED 1 + +#define MODE_VGA_640x480 0x01 +#define MODE_UXGA_1640x1232 0x02 +#define MODE_WQSXGA_3280x2464 0x03 +#define MODE_FHD_1920x1080 0x04 + +#define MIPI_DT_RAW8 0x2A +#define MIPI_DT_RAW10 0x2B \ No newline at end of file From addb276835cc1c762daf6b27f83ed49df9861792 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Wed, 24 May 2023 15:12:40 +0100 Subject: [PATCH 008/306] cleaning up sensor definitions and configurations --- sensors/api/sensor.h | 1 + sensors/api/sensor_defs.h | 2 +- sensors/imx219.h | 22 ++++++++++++++-------- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index fbbbd24a..e024875f 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -101,6 +101,7 @@ #define MIPI_LINE_WIDTH_BYTES MIPI_IMAGE_WIDTH_BYTES #define MIPI_MAX_PKT_SIZE_BYTES ((MIPI_LINE_WIDTH_BYTES) + 4) #define MIPI_TILE 1 +#define EXPECTED_FORMAT CONFIG_MIPI_FORMAT //backward compatibility // SRAM Image storage (do not edit) //TODO check maximum storage size for the image diff --git a/sensors/api/sensor_defs.h b/sensors/api/sensor_defs.h index 7d3f8b86..d1608598 100644 --- a/sensors/api/sensor_defs.h +++ b/sensors/api/sensor_defs.h @@ -8,4 +8,4 @@ #define MODE_FHD_1920x1080 0x04 #define MIPI_DT_RAW8 0x2A -#define MIPI_DT_RAW10 0x2B \ No newline at end of file +#define MIPI_DT_RAW10 0x2B diff --git a/sensors/imx219.h b/sensors/imx219.h index 90570549..c4f4af81 100644 --- a/sensors/imx219.h +++ b/sensors/imx219.h @@ -18,24 +18,30 @@ typedef struct #ifdef __XC__ // configure registers -#if ((CONFIG_MODE == MODE_VGA_RAW8) || (CONFIG_MODE == MODE_VGA_RAW10)) +#if (CONFIG_MODE == MODE_VGA_640x480) #define CONFIG_REG mode_640_480_regs -#elif (CONFIG_MODE == MODE_UXGA_RAW8) + +#elif (CONFIG_MODE == MODE_UXGA_1640x1232) #define CONFIG_REG mode_1640_1232_regs -#elif (CONFIG_MODE == MODE_WQSXGA_RAW8) + +#elif (CONFIG_MODE == MODE_WQSXGA_3280x2464) #define CONFIG_REG mode_3280x2464_regs -#elif (CONFIG_MODE == MODE_1920_1080) + +#elif (CONFIG_MODE == MODE_FHD_1920x1080) #define CONFIG_REG mode_1920_1080_regs + #else - #error "Invalid configuration mode" + #error CONFIG_MODE_NOT_VALID #endif // Configure formats -#if EXPECTED_FORMAT == MIPI_DT_RAW10 +#if CONFIG_MIPI_FORMAT == MIPI_DT_RAW10 #define DATA_FORMAT_REGS raw10_framefmt_regs -#elif EXPECTED_FORMAT == MIPI_DT_RAW8 +#elif CONFIG_MIPI_FORMAT == MIPI_DT_RAW8 #define DATA_FORMAT_REGS raw8_framefmt_regs +#else + #error CONFIG_MIPI_NOT_VALID #endif @@ -53,7 +59,7 @@ typedef struct #elif defined(FPS_76) #define PLL_VT_MPY 0x00E0 #else - #warning fps not defined, selecting default value + #warning "fps not defined, selecting default value" #define PLL_VT_MPY 0x0027 #endif From f6a8c439fe7204bc05176b1b8edea06b67085818 Mon Sep 17 00:00:00 2001 From: xalbertoisorna <124703429+xalbertoisorna@users.noreply.github.com> Date: Wed, 24 May 2023 15:51:37 +0100 Subject: [PATCH 009/306] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b776d222..aec81a97 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Camera framework -Repository to manipulate cameras using the XCORE.AI sensor +This repository serves as a comprehensive software solution for camera manipulation using the XCORE.AI sensor. ## Structure - **examples** : examples for taking pictures with the explorer board From 771d31802626b26d163fbe7eb584721914858e96 Mon Sep 17 00:00:00 2001 From: xalbertoisorna <124703429+xalbertoisorna@users.noreply.github.com> Date: Wed, 24 May 2023 15:52:43 +0100 Subject: [PATCH 010/306] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aec81a97..534880a4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Camera framework This repository serves as a comprehensive software solution for camera manipulation using the XCORE.AI sensor. -## Structure +## Repository Structure - **examples** : examples for taking pictures with the explorer board - **lib_camera** : useful functions to manipulate images - **modules** : dependencies folder @@ -23,6 +23,8 @@ git clone --recurse-submodules git@github.com:xmos/fwk_camera.git ## Build ``` sh launch_cmake.sh +cd build/ +make {YOUR_EXAMPLE} ``` ## Useful commands From 8132dd88c07a3fdaa384d108f08b51d5c4620f5a Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Thu, 25 May 2023 14:32:24 +0100 Subject: [PATCH 011/306] XV-80 adding YUV to RGB --- camera/CMakeLists.txt | 2 +- camera/asm/rgb_to_yuv.S | 63 +++++++++++++++++++ camera/asm/yuv_to_rgb.S | 57 +++++++++++++++++ camera/isp.h | 16 ++++- .../src/process_frame.c | 1 + 5 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 camera/asm/rgb_to_yuv.S create mode 100644 camera/asm/yuv_to_rgb.S diff --git a/camera/CMakeLists.txt b/camera/CMakeLists.txt index cf18e07f..8c935d51 100644 --- a/camera/CMakeLists.txt +++ b/camera/CMakeLists.txt @@ -17,7 +17,7 @@ file(GLOB_RECURSE SOURCES_ASM "*.S") add_library(${LIB_NAME} STATIC) -target_include_directories(${LIB_NAME} PUBLIC .) +target_include_directories(${LIB_NAME} PUBLIC . asm) target_sources(${LIB_NAME} PRIVATE diff --git a/camera/asm/rgb_to_yuv.S b/camera/asm/rgb_to_yuv.S new file mode 100644 index 00000000..269a345c --- /dev/null +++ b/camera/asm/rgb_to_yuv.S @@ -0,0 +1,63 @@ +/* +Converts signed yuv (-127..127, -127..127, -127..127) into signed rgb. +Returns an int with 3 x int8_t + + int rgb_to_yuv( + int r, + int g, + int b); + +*/ + +.cc_top rgb_to_yuv.func, rgb_to_yuv +.align 4 +.globl rgb_to_yuv +.globl rgb_to_yuv.nstackwords +.linkset rgb_to_yuv.nstackwords, 4 +.issue_mode dual + + +#define r r0 +#define g r1 +#define b r2 + +rgb_to_yuv: +{ ldc r11, 0 ; dualentsp 8 } // Enable dual instructions +{ ldc r3, 22 ; vsetc r11 } // Load 22 in R3 ; control register and the headroom in the vector unit. Signed 32-bit integer +{ shl r, r, r3 ; ldaw r11, sp[0] } // Multiply by 2^22; load word in the Sp + +{ shl g, g, r3 ; stw r, r11[0] } // Multiply by 2^22; store y in the vpu +{ shl b, b, r3 ; stw g, r11[1] } // Multiply by 2^22; store u in the vpu +{ ldc r3, 32 ; stw b, r11[2] } // Load constant; store v in the vpu + +#undef y +#undef u +#undef v + +{ ldc r1, 24 ; vclrdr } // load in R1 24 ; Sets the contents of vD and vR in the vector unit to all zeroes. +{ neg r1, r1 ; vldc r11[0] } // signed negation ; load first opdfand +ldap r11, Yconv // Load effective address relative to the program counter. +{ add r11, r11, r3 ; vlmaccr r11[0] } +{ add r11, r11, r3 ; vlmaccr r11[0] } +{ ldaw r11, sp[0] ; vlmaccr r11[0] } + +{ ; vstr r11[0] } + vlashr r11[0], r1 +{ ; vdepth8 } +{ ; vstr r11[0] } +{ ; ldw r0, sp[0] } +retsp 8 + +// the values correspond to the matrix values *255 +// https://en.wikipedia.org/wiki/YUV +// https://softpixel.com/~cwright/programming/colorspace/yuv/ +/* +Yconv: .word 157, -131, -26, 0, 0,0,0,0 +grnConv: .word -38, -74, 111, 0, 0,0,0,0 +redConv: .word 76, 150, 29, 0, 0,0,0,0 +*/ +Yconv: .word 128, -107, -21, 0, 0,0,0,0 +Uconv: .word -43, -84, 128, 0, 0,0,0,0 +Vconv: .word 76, 150, 29, 0, 0,0,0,0 + +.cc_bottom rgb_to_yuv.func diff --git a/camera/asm/yuv_to_rgb.S b/camera/asm/yuv_to_rgb.S new file mode 100644 index 00000000..939259ae --- /dev/null +++ b/camera/asm/yuv_to_rgb.S @@ -0,0 +1,57 @@ +/* +Converts signed yuv (-127..127, -127..127, -127..127) into signed rgb. +Returns an int with 3 x int8_t + + int yuv_to_rgb( + int y, + int u, + int v); + +*/ + +.cc_top yuv_to_rgb.func, yuv_to_rgb +.align 4 +.globl yuv_to_rgb +.globl yuv_to_rgb.nstackwords +.linkset yuv_to_rgb.nstackwords, 4 +.issue_mode dual + + +#define y r0 +#define u r1 +#define v r2 + +yuv_to_rgb: +{ ldc r11, 0 ; dualentsp 8 } // Enable dual instructions +{ ldc r3, 22 ; vsetc r11 } // Load 22 in R3 ; control register and the headroom in the vector unit. Signed 32-bit integer +{ shl y, y, r3 ; ldaw r11, sp[0] } // Multiply by 2^22; load word in the Sp + +{ shl u, u, r3 ; stw y, r11[0] } // Multiply by 2^22; store y in the vpu +{ shl v, v, r3 ; stw u, r11[1] } // Multiply by 2^22; store u in the vpu +{ ldc r3, 32 ; stw v, r11[2] } // Load constant; store v in the vpu + +#undef y +#undef u +#undef v + +{ ldc r1, 24 ; vclrdr } // load in R1 24 ; Sets the contents of vD and vR in the vector unit to all zeroes. +{ neg r1, r1 ; vldc r11[0] } // signed negation ; load first opdfand +ldap r11, bluConv // Load effective address relative to the program counter. +{ add r11, r11, r3 ; vlmaccr r11[0] } +{ add r11, r11, r3 ; vlmaccr r11[0] } +{ ldaw r11, sp[0] ; vlmaccr r11[0] } + +{ ; vstr r11[0] } + vlashr r11[0], r1 +{ ; vdepth8 } +{ ; vstr r11[0] } +{ ; ldw r0, sp[0] } +retsp 8 + +// the values correspond to the matrix values *255 +// https://en.wikipedia.org/wiki/YUV +bluConv: .word 256, 520, 0, 0, 0,0,0,0 +grnConv: .word 256, -101, -149, 0, 0,0,0,0 +redConv: .word 256, 0, 292, 0, 0,0,0,0 + +.cc_bottom yuv_to_rgb.func diff --git a/camera/isp.h b/camera/isp.h index 99c75a6e..43de49e7 100644 --- a/camera/isp.h +++ b/camera/isp.h @@ -7,14 +7,16 @@ #include "statistics.h" // for dynamic AWB + // ----------------- ISP SETTINGS ------------------------------- +#define MASK8 0xFF +#define MASK24 0x00FFFFFF #define DEFAULT_ALFA 1.58682 // default red gain (not used for the moment) #define DEFAULT_BETA 1.52535 // default blue gain (not used for the moment) #define ENABLE_AE 1 // enable auto exposure #define AE_MARGIN 0.1 // default marging for the auto exposure error #define STEP 16 // histogram step size - // ---------------------------------- AE/AGC ------------------------------ uint8_t csign(float x); uint8_t isp_false_position_step(float exposure, float skewness); @@ -36,4 +38,14 @@ void isp_bilinear_resize(const uint16_t in_width, const uint16_t in_height, void isp_rotate_image(const uint8_t* src, uint8_t* dest, int width, int height); -#endif \ No newline at end of file +// -------------------------- COLOR CONVERSION ------------------------------------- +// Macro arguments +#define GET_R(x) ((int8_t)((x & MASK24)) & MASK8); +#define GET_G(x) ((int8_t)((x & MASK24) >> 8) & MASK8); +#define GET_B(x) ((int8_t)((x & MASK24) >> 16) & MASK8); + +// Converts signed yuv or rgb (-127..127, -127..127, -127..127) into signed rgb / yuv. +extern int yuv_to_rgb(int y, int u, int v); +extern int rgb_to_yuv(int r, int g, int b); + +#endif // ISP_H \ No newline at end of file diff --git a/examples/take_picture_dynamic_crop/src/process_frame.c b/examples/take_picture_dynamic_crop/src/process_frame.c index a2a77ff9..ce3028ab 100644 --- a/examples/take_picture_dynamic_crop/src/process_frame.c +++ b/examples/take_picture_dynamic_crop/src/process_frame.c @@ -40,6 +40,7 @@ void write_image(uint8_t *image) printf("Outfile %s\n", FINAL_IMAGE_FILENAME); printf("image size (%dx%d)\n", IMG_W, IMG_H); free(image); + yuv_to_rgb(2,2,2); exit(1); } From 77c1da89efb07de098159a8e53eeb660f18eef0c Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Thu, 25 May 2023 17:26:37 +0100 Subject: [PATCH 012/306] first unit test example --- CMakeLists.txt | 3 + camera/asm/rgb_to_yuv.S | 10 +- camera/isp.h | 9 +- .../take_picture_dynamic.cmake | 1 + sensors/imx219.h | 2 +- sensors/imx219_reg.h | 2 +- tests/CMakeLists.txt | 2 + tests/Unity | 1 + tests/unit_test_camera/CMakeLists.txt | 46 ++++++++ tests/unit_test_camera/XCORE-AI-EXPLORER.xn | 109 ++++++++++++++++++ tests/unit_test_camera/config.xscope | 22 ++++ tests/unit_test_camera/src/test_isp.c | 53 +++++++++ tests/{dummy => unit_test_sensor/.gitkeep} | 0 13 files changed, 252 insertions(+), 8 deletions(-) create mode 100644 tests/CMakeLists.txt create mode 160000 tests/Unity create mode 100644 tests/unit_test_camera/CMakeLists.txt create mode 100644 tests/unit_test_camera/XCORE-AI-EXPLORER.xn create mode 100644 tests/unit_test_camera/config.xscope create mode 100644 tests/unit_test_camera/src/test_isp.c rename tests/{dummy => unit_test_sensor/.gitkeep} (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index b2bdf609..471d173c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,9 @@ add_subdirectory(sensors) ## Add cameras add_subdirectory(camera) +## Add tests +add_subdirectory(tests) + ## Add top level project targets if(PROJECT_IS_TOP_LEVEL) include(examples/examples.cmake) diff --git a/camera/asm/rgb_to_yuv.S b/camera/asm/rgb_to_yuv.S index 269a345c..8cf80ded 100644 --- a/camera/asm/rgb_to_yuv.S +++ b/camera/asm/rgb_to_yuv.S @@ -1,7 +1,6 @@ /* Converts signed yuv (-127..127, -127..127, -127..127) into signed rgb. Returns an int with 3 x int8_t - int rgb_to_yuv( int r, int g, @@ -49,8 +48,6 @@ ldap r11, Yconv // Load effective retsp 8 // the values correspond to the matrix values *255 -// https://en.wikipedia.org/wiki/YUV -// https://softpixel.com/~cwright/programming/colorspace/yuv/ /* Yconv: .word 157, -131, -26, 0, 0,0,0,0 grnConv: .word -38, -74, 111, 0, 0,0,0,0 @@ -61,3 +58,10 @@ Uconv: .word -43, -84, 128, 0, 0,0,0,0 Vconv: .word 76, 150, 29, 0, 0,0,0,0 .cc_bottom rgb_to_yuv.func + + +// References +// https://en.wikipedia.org/wiki/YUV +// https://softpixel.com/~cwright/programming/colorspace/yuv/ +// https://www.mikekohn.net/file_formats/yuv_rgb_converter.php +// \ No newline at end of file diff --git a/camera/isp.h b/camera/isp.h index 43de49e7..07146489 100644 --- a/camera/isp.h +++ b/camera/isp.h @@ -40,9 +40,12 @@ void isp_rotate_image(const uint8_t* src, uint8_t* dest, int width, int height); // -------------------------- COLOR CONVERSION ------------------------------------- // Macro arguments -#define GET_R(x) ((int8_t)((x & MASK24)) & MASK8); -#define GET_G(x) ((int8_t)((x & MASK24) >> 8) & MASK8); -#define GET_B(x) ((int8_t)((x & MASK24) >> 16) & MASK8); +#define GET_R(x) ((int8_t)((x & MASK24)) & MASK8) +#define GET_G(x) ((int8_t)((x & MASK24) >> 8) & MASK8) +#define GET_B(x) ((int8_t)((x & MASK24) >> 16) & MASK8) +#define GET_Y(x) GET_R(x) +#define GET_U(x) GET_G(x) +#define GET_V(x) GET_B(x) // Converts signed yuv or rgb (-127..127, -127..127, -127..127) into signed rgb / yuv. extern int yuv_to_rgb(int y, int u, int v); diff --git a/examples/take_picture_dynamic/take_picture_dynamic.cmake b/examples/take_picture_dynamic/take_picture_dynamic.cmake index ed3c50ab..7b2dfcc8 100644 --- a/examples/take_picture_dynamic/take_picture_dynamic.cmake +++ b/examples/take_picture_dynamic/take_picture_dynamic.cmake @@ -24,6 +24,7 @@ set(APP_COMPILER_FLAGS ${CMAKE_CURRENT_LIST_DIR}/src/config.xscope ${CMAKE_CURRENT_LIST_DIR}/XCORE-AI-EXPLORER.xn ) + set(APP_COMPILE_DEFINITIONS configENABLE_DEBUG_PRINTF=1 PLATFORM_SUPPORTS_TILE_0=1 diff --git a/sensors/imx219.h b/sensors/imx219.h index c4f4af81..3eb4e085 100644 --- a/sensors/imx219.h +++ b/sensors/imx219.h @@ -47,7 +47,7 @@ typedef struct // configure FPS #if defined(FPS_13) - #define PLL_VT_MPY 0x0020 + #define PLL_VT_MPY 0x0017 #elif defined(FPS_24) #define PLL_VT_MPY 0x0047 #elif defined(FPS_30) diff --git a/sensors/imx219_reg.h b/sensors/imx219_reg.h index 927c8eeb..0df18338 100644 --- a/sensors/imx219_reg.h +++ b/sensors/imx219_reg.h @@ -68,7 +68,7 @@ static imx219_settings_t imx219_common_regs[] = { { 0x812A, 0x1800 }, // EXCK_FREQ 24.00, for 24 Mhz { 0x0304, 0x02 }, // PREPLLCK_VT_DIV 2, for pre divide by 2 { 0x0305, 0x02 }, // PREPLLCK_OP_DIV 2, for pre divide by 2 - { 0x8306, 0x0027}, // PLL_VT_MPY 0x27, for multiply by 39, pixclk=187.2 MHz + { 0x8306, PLL_VT_MPY}, // PLL_VT_MPY 0x27, for multiply by 39, pixclk=187.2 MHz { 0x830C, 0x40}, // PLL_OP_MPY 0x40, for multiply by 64, MIPI clk=768 MHz { 0x0301, VTPXCK_DIV}, // VTPXCK_DIV 5, // Options: 05, 08 or 0A (higher the slowest) { 0x0303, 0x01 }, // VTSYCK_DIV 1, ? diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 00000000..58e40fa6 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(Unity) +add_subdirectory(unit_test_camera) diff --git a/tests/Unity b/tests/Unity new file mode 160000 index 00000000..0e50926b --- /dev/null +++ b/tests/Unity @@ -0,0 +1 @@ +Subproject commit 0e50926b52d8f27bfaa3b7c43e04c98f4f98ad21 diff --git a/tests/unit_test_camera/CMakeLists.txt b/tests/unit_test_camera/CMakeLists.txt new file mode 100644 index 00000000..951b0890 --- /dev/null +++ b/tests/unit_test_camera/CMakeLists.txt @@ -0,0 +1,46 @@ +# ############################################################################## +# Sources and definitions +# ############################################################################## +set(TARGET test_camera) + +file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.*c) + +set(APP_COMMON_LINK_LIBRARIES + tests::Unity + camera::lib_camera + ) + +# ############################################################################## +# Flags +# ############################################################################## +set(APP_LINK_OPTIONS -report + ${CMAKE_CURRENT_LIST_DIR}/XCORE-AI-EXPLORER.xn + ${CMAKE_CURRENT_LIST_DIR}/config.xscope +) + +set(APP_COMPILER_FLAGS + -Os + -g + -report + -fxscope + -mcmodel=large + -Wno-xcore-fptrgroup + ${CMAKE_CURRENT_LIST_DIR}/config.xscope + ${CMAKE_CURRENT_LIST_DIR}/XCORE-AI-EXPLORER.xn +) + +set(APP_COMPILE_DEFINITIONS + configENABLE_DEBUG_PRINTF=1 + XUD_CORE_CLOCK=600) + + +# ############################################################################## +# Create executable +# ############################################################################## +add_executable(${TARGET} EXCLUDE_FROM_ALL) +target_sources(${TARGET} PUBLIC ${APP_SOURCES}) +target_include_directories(${TARGET} PUBLIC ${APP_INCLUDES}) +target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) +target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) +target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) +target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) \ No newline at end of file diff --git a/tests/unit_test_camera/XCORE-AI-EXPLORER.xn b/tests/unit_test_camera/XCORE-AI-EXPLORER.xn new file mode 100644 index 00000000..b7d3cd93 --- /dev/null +++ b/tests/unit_test_camera/XCORE-AI-EXPLORER.xn @@ -0,0 +1,109 @@ + + + Board + xcore.ai Explorer Kit + + + tileref tile[2] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/unit_test_camera/config.xscope b/tests/unit_test_camera/config.xscope new file mode 100644 index 00000000..69236026 --- /dev/null +++ b/tests/unit_test_camera/config.xscope @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/unit_test_camera/src/test_isp.c b/tests/unit_test_camera/src/test_isp.c new file mode 100644 index 00000000..ecc289c8 --- /dev/null +++ b/tests/unit_test_camera/src/test_isp.c @@ -0,0 +1,53 @@ +#include + +#include "unity.h" +#include "isp.h" + +void setUp(void) { + // set stuff up here +} + +void tearDown(void) { + // clean stuff up here +} + +void test_yuv_to_rgb(void) { + int Y = 76; + int U = 84; + int V = 255; + Y -= 127; + U -= 127; + V -= 127; + + uint32_t result = yuv_to_rgb(Y,U,V); + + // Printing the extracted bytes + TEST_ASSERT_EQUAL_UINT8(222, (uint8_t)(GET_R(result)+127)); + TEST_ASSERT_EQUAL_UINT8(19, (uint8_t)(GET_G(result)+127)); + TEST_ASSERT_EQUAL_UINT8(0, (uint8_t)(GET_B(result)+127)); +} + +void test_rgb_to_yuv(void) { + int R = 222; + int G = 19; + int B = 0; + R -= 127; + G -= 127; + B -= 127; + + uint32_t result = rgb_to_yuv(R,G,B); + + // Printing the extracted bytes + TEST_ASSERT_EQUAL_UINT8(78, (uint8_t)(GET_Y(result)+127)); + TEST_ASSERT_EQUAL_UINT8(83, (uint8_t)(GET_U(result)+127)); + TEST_ASSERT_EQUAL_UINT8(230, (uint8_t)(GET_V(result)+127)); +} + + +// ---------------------------------------------------------------- +int main(void) { + UNITY_BEGIN(); + RUN_TEST(test_rgb_to_yuv); + RUN_TEST(test_yuv_to_rgb); + return UNITY_END(); +} \ No newline at end of file diff --git a/tests/dummy b/tests/unit_test_sensor/.gitkeep similarity index 100% rename from tests/dummy rename to tests/unit_test_sensor/.gitkeep From 0f6e54a3f87493f3603bc4ca20344f50c2e1c4c7 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Fri, 26 May 2023 12:12:50 +0100 Subject: [PATCH 013/306] adding cmake lists for lib_i2c and lib_xassert and pointing them to origin --- .gitmodules | 7 +++++-- modules/CMakeLists.txt | 4 +++- modules/i2c | 2 +- modules/xassert | 1 + 4 files changed, 10 insertions(+), 4 deletions(-) create mode 160000 modules/xassert diff --git a/.gitmodules b/.gitmodules index cb0fc2c7..855b0837 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,10 +1,10 @@ [submodule "xmos_cmake_toolchain"] path = xmos_cmake_toolchain - url = git@github.com:xalbertoisorna/xmos_cmake_toolchain # grouping some CMAKE utilities + url = git@github.com:xmos/xmos_cmake_toolchain # grouping some CMAKE utilities branch = main [submodule "modules/i2c"] path = modules/i2c - url = git@github.com:xalbertoisorna/lib_i2c # adding lib assert and cmake to the repo + url = git@github.com:xmos/lib_i2c # adding lib assert and cmake to the repo branch = develop [submodule "modules/mipi"] path = modules/mipi @@ -14,3 +14,6 @@ path = modules/core url = git@github.com:xmos/fwk_core # xcore math inside the repo branch = develop +[submodule "modules/xassert"] + path = modules/xassert + url = git@github.com:xmos/lib_xassert.git diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index 33dff2fa..c8dfbb60 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -1,3 +1,5 @@ add_subdirectory(core) add_subdirectory(mipi) -add_subdirectory(i2c) +#add_subdirectory(i2c) +include(xassert.cmake) +include(i2c.cmake) diff --git a/modules/i2c b/modules/i2c index 05952714..776d465d 160000 --- a/modules/i2c +++ b/modules/i2c @@ -1 +1 @@ -Subproject commit 05952714dc0092f87f6610098ad3bbaea8b277fb +Subproject commit 776d465d604783f2a7072b3d406466d184d2fa21 diff --git a/modules/xassert b/modules/xassert new file mode 160000 index 00000000..03476829 --- /dev/null +++ b/modules/xassert @@ -0,0 +1 @@ +Subproject commit 0347682944a2a70fad016d2dc55072189617e4ba From f73adc990e520ae6eb10a309605922c10bda133f Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Fri, 26 May 2023 12:13:20 +0100 Subject: [PATCH 014/306] adding missing scripts --- modules/i2c.cmake | 33 +++++++++++++++++++++++++++++++++ modules/xassert.cmake | 23 +++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 modules/i2c.cmake create mode 100644 modules/xassert.cmake diff --git a/modules/i2c.cmake b/modules/i2c.cmake new file mode 100644 index 00000000..a9a2c1fc --- /dev/null +++ b/modules/i2c.cmake @@ -0,0 +1,33 @@ + +set(LIB_NAME lib_i2c) +set(LIB_I2C_PATH ${CMAKE_CURRENT_LIST_DIR}/i2c/lib_i2c/) + +add_library(${LIB_NAME} STATIC) + +target_sources(${LIB_NAME} + PRIVATE + ${LIB_I2C_PATH}/src/i2c_master_async.xc + ${LIB_I2C_PATH}/src/i2c_master_ext.xc + ${LIB_I2C_PATH}/src/i2c_master_single_port.xc + ${LIB_I2C_PATH}/src/i2c_master.xc + ${LIB_I2C_PATH}/src/i2c_slave.xc +) + +target_include_directories(${LIB_NAME} + PUBLIC + ${LIB_I2C_PATH}/api +) + +target_compile_options(${LIB_NAME} + PRIVATE + -Os -g + -Wno-format + -Wno-unused-variable + -Wno-missing-braces +) + +target_link_libraries( ${LIB_NAME} + lib_xassert +) + +add_library(i2c::lib_i2c ALIAS ${LIB_NAME}) diff --git a/modules/xassert.cmake b/modules/xassert.cmake new file mode 100644 index 00000000..1a25e058 --- /dev/null +++ b/modules/xassert.cmake @@ -0,0 +1,23 @@ + +set(LIB_NAME lib_xassert) +set(LIB_XASSERT_PATH ${CMAKE_CURRENT_LIST_DIR}/xassert/lib_xassert) + +add_library(${LIB_NAME} STATIC) + +target_sources(${LIB_NAME} + PRIVATE + ${LIB_XASSERT_PATH}/src/xassert.xc +) + +target_include_directories(${LIB_NAME} + PUBLIC + ${LIB_XASSERT_PATH}/api +) + +target_compile_options(${LIB_NAME} + PRIVATE + -Os -g + -Wno-format + -Wno-unused-variable + -Wno-missing-braces +) From a9ce698a7384634c7876386c2b35f99612782aaf Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Fri, 26 May 2023 12:39:52 +0100 Subject: [PATCH 015/306] adding comments in cmake --- modules/CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index c8dfbb60..cf309c8c 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -1,5 +1,7 @@ +# adding submodules here add_subdirectory(core) add_subdirectory(mipi) -#add_subdirectory(i2c) +# lib_i2c and lib_xassert do not have CMakeLists.txt +# so using wrappers here include(xassert.cmake) include(i2c.cmake) From ab6ba7621dfce0b00ec18701a54be26cef45b813 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Fri, 26 May 2023 15:41:32 +0100 Subject: [PATCH 016/306] adding rgb to yuv conversion --- camera/asm/rgb_to_yuv.S | 15 ++--- tests/CMakeLists.txt | 7 ++- tests/unit_test_camera/src/test_isp.c | 53 ---------------- tests/unit_test_sensor/.gitkeep | 0 .../CMakeLists.txt | 6 +- .../XCORE-AI-EXPLORER.xn | 0 .../config.xscope | 0 tests/unit_tests/src/test_isp.c | 62 +++++++++++++++++++ tests/unit_tests/src/test_isp.h | 34 ++++++++++ tests/unity.cmake | 27 ++++++++ 10 files changed, 141 insertions(+), 63 deletions(-) delete mode 100644 tests/unit_test_camera/src/test_isp.c delete mode 100644 tests/unit_test_sensor/.gitkeep rename tests/{unit_test_camera => unit_tests}/CMakeLists.txt (91%) rename tests/{unit_test_camera => unit_tests}/XCORE-AI-EXPLORER.xn (100%) rename tests/{unit_test_camera => unit_tests}/config.xscope (100%) create mode 100644 tests/unit_tests/src/test_isp.c create mode 100644 tests/unit_tests/src/test_isp.h create mode 100644 tests/unity.cmake diff --git a/camera/asm/rgb_to_yuv.S b/camera/asm/rgb_to_yuv.S index 8cf80ded..a623317d 100644 --- a/camera/asm/rgb_to_yuv.S +++ b/camera/asm/rgb_to_yuv.S @@ -35,7 +35,7 @@ rgb_to_yuv: { ldc r1, 24 ; vclrdr } // load in R1 24 ; Sets the contents of vD and vR in the vector unit to all zeroes. { neg r1, r1 ; vldc r11[0] } // signed negation ; load first opdfand -ldap r11, Yconv // Load effective address relative to the program counter. +ldap r11, Vconv // Load effective address relative to the program counter. { add r11, r11, r3 ; vlmaccr r11[0] } { add r11, r11, r3 ; vlmaccr r11[0] } { ldaw r11, sp[0] ; vlmaccr r11[0] } @@ -49,13 +49,14 @@ retsp 8 // the values correspond to the matrix values *255 /* -Yconv: .word 157, -131, -26, 0, 0,0,0,0 -grnConv: .word -38, -74, 111, 0, 0,0,0,0 -redConv: .word 76, 150, 29, 0, 0,0,0,0 +Yconv: .word 157, -132, -26, 0, 0,0,0,0 +Uconv: .word -38, -74, 112, 0, 0,0,0,0 +Vconv: .word 77, 150, 29, 0, 0,0,0,0 */ -Yconv: .word 128, -107, -21, 0, 0,0,0,0 -Uconv: .word -43, -84, 128, 0, 0,0,0,0 -Vconv: .word 76, 150, 29, 0, 0,0,0,0 + +Vconv: .word 128, -107, -21, 0, 0,0,0,0 +Uconv: .word -43, -85, 128, 0, 0,0,0,0 +Yconv: .word 77, 150, 29, 0, 0,0,0,0 .cc_bottom rgb_to_yuv.func diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 58e40fa6..f2394275 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,2 +1,5 @@ -add_subdirectory(Unity) -add_subdirectory(unit_test_camera) +add_subdirectory(unit_tests) + +# Xmos/Unity does not have cmake +# so using wrappers here +include(unity.cmake) diff --git a/tests/unit_test_camera/src/test_isp.c b/tests/unit_test_camera/src/test_isp.c deleted file mode 100644 index ecc289c8..00000000 --- a/tests/unit_test_camera/src/test_isp.c +++ /dev/null @@ -1,53 +0,0 @@ -#include - -#include "unity.h" -#include "isp.h" - -void setUp(void) { - // set stuff up here -} - -void tearDown(void) { - // clean stuff up here -} - -void test_yuv_to_rgb(void) { - int Y = 76; - int U = 84; - int V = 255; - Y -= 127; - U -= 127; - V -= 127; - - uint32_t result = yuv_to_rgb(Y,U,V); - - // Printing the extracted bytes - TEST_ASSERT_EQUAL_UINT8(222, (uint8_t)(GET_R(result)+127)); - TEST_ASSERT_EQUAL_UINT8(19, (uint8_t)(GET_G(result)+127)); - TEST_ASSERT_EQUAL_UINT8(0, (uint8_t)(GET_B(result)+127)); -} - -void test_rgb_to_yuv(void) { - int R = 222; - int G = 19; - int B = 0; - R -= 127; - G -= 127; - B -= 127; - - uint32_t result = rgb_to_yuv(R,G,B); - - // Printing the extracted bytes - TEST_ASSERT_EQUAL_UINT8(78, (uint8_t)(GET_Y(result)+127)); - TEST_ASSERT_EQUAL_UINT8(83, (uint8_t)(GET_U(result)+127)); - TEST_ASSERT_EQUAL_UINT8(230, (uint8_t)(GET_V(result)+127)); -} - - -// ---------------------------------------------------------------- -int main(void) { - UNITY_BEGIN(); - RUN_TEST(test_rgb_to_yuv); - RUN_TEST(test_yuv_to_rgb); - return UNITY_END(); -} \ No newline at end of file diff --git a/tests/unit_test_sensor/.gitkeep b/tests/unit_test_sensor/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/unit_test_camera/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt similarity index 91% rename from tests/unit_test_camera/CMakeLists.txt rename to tests/unit_tests/CMakeLists.txt index 951b0890..f83cef1d 100644 --- a/tests/unit_test_camera/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -43,4 +43,8 @@ target_include_directories(${TARGET} PUBLIC ${APP_INCLUDES}) target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) -target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) \ No newline at end of file +target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) + + +# enable testing functionality +add_test(NAME ${TARGET} COMMAND ${TARGET}) \ No newline at end of file diff --git a/tests/unit_test_camera/XCORE-AI-EXPLORER.xn b/tests/unit_tests/XCORE-AI-EXPLORER.xn similarity index 100% rename from tests/unit_test_camera/XCORE-AI-EXPLORER.xn rename to tests/unit_tests/XCORE-AI-EXPLORER.xn diff --git a/tests/unit_test_camera/config.xscope b/tests/unit_tests/config.xscope similarity index 100% rename from tests/unit_test_camera/config.xscope rename to tests/unit_tests/config.xscope diff --git a/tests/unit_tests/src/test_isp.c b/tests/unit_tests/src/test_isp.c new file mode 100644 index 00000000..336c3de7 --- /dev/null +++ b/tests/unit_tests/src/test_isp.c @@ -0,0 +1,62 @@ +#include +#include "test_isp.h" + +color_table_t ct_test = {16, 100, 16, 65, 100, 92}; // R G B Y U V +//TODO include more colors for testing + + +void setUp(void) +{ + // set stuff up here +} + +void tearDown(void) +{ + // clean stuff up here +} + +void test_yuv_to_rgb() +{ + // Define color table + color_table_t ct_ref = ct_test; + color_table_t ct_result; + + // Test the converison + yuv_to_rgb_ct(&ct_ref, &ct_result); + + printColorTable(&ct_test); + printColorTable(&ct_result); + + // Ensure conversion is correct + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.R, ct_result.R); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.G, ct_result.G); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.B, ct_result.B); +} + +void test_rgb_to_yuv() +{ + // Define color table + color_table_t ct_ref = ct_test; + color_table_t ct_result; + + // Test the converison + rgb_to_yuv_ct(&ct_ref, &ct_result); + + printColorTable(&ct_test); + printColorTable(&ct_result); + + // Printing the extracted bytes + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.Y, ct_result.Y); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.U, ct_result.U); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.V, ct_result.V); +} + + +// ---------------------------------------------------------------- +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_rgb_to_yuv); + RUN_TEST(test_yuv_to_rgb); + return UNITY_END(); +} \ No newline at end of file diff --git a/tests/unit_tests/src/test_isp.h b/tests/unit_tests/src/test_isp.h new file mode 100644 index 00000000..3d1b61bd --- /dev/null +++ b/tests/unit_tests/src/test_isp.h @@ -0,0 +1,34 @@ +#include "unity.h" +#include "isp.h" + +#define INV_DELTA 30 // error allowed in YUV RGB color conversion +#define CT_INT 127 // int conversion + +// Store the RGB color and corresponding values +typedef struct +{ + int R, G, B; + int Y, U, V; +} color_table_t; + +void printColorTable(color_table_t* table) { + printf("Color Table:\n"); + printf("R: %d, G: %d, B: %d\n", table->R, table->G, table->B); + printf("Y: %d, U: %d, V: %d\n", table->Y, table->U, table->V); +} + +void yuv_to_rgb_ct(color_table_t* ct_ref, color_table_t* ct_res){ + *ct_res = *ct_ref; + uint32_t result = yuv_to_rgb(ct_ref->Y - CT_INT, ct_ref->U - CT_INT, ct_ref->V - CT_INT); + ct_res -> R = (uint8_t)(GET_R(result) + CT_INT); + ct_res -> G = (uint8_t)(GET_G(result) + CT_INT); + ct_res -> B = (uint8_t)(GET_B(result) + CT_INT); +} + +void rgb_to_yuv_ct(color_table_t* ct_ref, color_table_t* ct_res){ + *ct_res = *ct_ref; + uint32_t result = rgb_to_yuv(ct_ref->Y - CT_INT, ct_ref->U - CT_INT, ct_ref->V - CT_INT); + ct_res -> Y = (uint8_t)(GET_Y(result) + CT_INT); + ct_res -> U = (uint8_t)(GET_U(result) + CT_INT); + ct_res -> V = (uint8_t)(GET_V(result) + CT_INT); +} \ No newline at end of file diff --git a/tests/unity.cmake b/tests/unity.cmake new file mode 100644 index 00000000..2c86605a --- /dev/null +++ b/tests/unity.cmake @@ -0,0 +1,27 @@ +# ############################################################################## +# Target name +set(LIB_NAME lib_Unity) +set(LIB_PATH ${CMAKE_CURRENT_LIST_DIR}/Unity/src) + +# Source files +file(GLOB_RECURSE SOURCES_C ${LIB_PATH} *.c) +file(GLOB_RECURSE SOURCES_XC ${LIB_PATH} *.xc) +file(GLOB_RECURSE SOURCES_CPP ${LIB_PATH} *.cpp) +file(GLOB_RECURSE SOURCES_ASM ${LIB_PATH} *.S) + +add_library(${LIB_NAME} STATIC) + +target_include_directories( + ${LIB_NAME} PUBLIC + ${CMAKE_CURRENT_LIST_DIR}/Unity/src/ +) + +target_sources(${LIB_NAME} + PRIVATE + ${SOURCES_C} + ${SOURCES_XC} + ${SOURCES_CPP} + ${SOURCES_ASM} +) + +add_library(tests::Unity ALIAS ${LIB_NAME}) From a4d90a539cd54967c932dc75f6efe3286e47dadd Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Fri, 26 May 2023 15:46:05 +0100 Subject: [PATCH 017/306] XV-87 adding dummy test --- .gitmodules | 4 ++++ tests/unity.cmake | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.gitmodules b/.gitmodules index cb0fc2c7..a0d094a1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,3 +14,7 @@ path = modules/core url = git@github.com:xmos/fwk_core # xcore math inside the repo branch = develop +[submodule "tests/Unity"] + path = tests/Unity + url = git@github.com:xmos/Unity # unit testing + branch = develop \ No newline at end of file diff --git a/tests/unity.cmake b/tests/unity.cmake index 2c86605a..14c95614 100644 --- a/tests/unity.cmake +++ b/tests/unity.cmake @@ -4,10 +4,10 @@ set(LIB_NAME lib_Unity) set(LIB_PATH ${CMAKE_CURRENT_LIST_DIR}/Unity/src) # Source files -file(GLOB_RECURSE SOURCES_C ${LIB_PATH} *.c) -file(GLOB_RECURSE SOURCES_XC ${LIB_PATH} *.xc) -file(GLOB_RECURSE SOURCES_CPP ${LIB_PATH} *.cpp) -file(GLOB_RECURSE SOURCES_ASM ${LIB_PATH} *.S) +file(GLOB_RECURSE SOURCES_C ${LIB_PATH}/*.c) +file(GLOB_RECURSE SOURCES_XC ${LIB_PATH}/*.xc) +file(GLOB_RECURSE SOURCES_CPP ${LIB_PATH}/*.cpp) +file(GLOB_RECURSE SOURCES_ASM ${LIB_PATH}/*.S) add_library(${LIB_NAME} STATIC) From eb9df08a9591fcc24dbb4668b3b49ae5d54b0cc7 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Fri, 26 May 2023 17:17:28 +0100 Subject: [PATCH 018/306] changing git module to original unity git --- .gitmodules | 2 +- tests/CMakeLists.txt | 6 ++---- tests/Unity | 2 +- tests/unit_tests/CMakeLists.txt | 4 ++-- tests/unity.cmake | 4 ++-- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.gitmodules b/.gitmodules index a0d094a1..d2f04329 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,5 +16,5 @@ branch = develop [submodule "tests/Unity"] path = tests/Unity - url = git@github.com:xmos/Unity # unit testing + url = https://github.com/ThrowTheSwitch/Unity.git # unit testing branch = develop \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f2394275..b8b110d8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,5 +1,3 @@ add_subdirectory(unit_tests) - -# Xmos/Unity does not have cmake -# so using wrappers here -include(unity.cmake) +#add_subdirectory(Unity) +include(unity.cmake) # custom cmake file diff --git a/tests/Unity b/tests/Unity index 0e50926b..f23d8b25 160000 --- a/tests/Unity +++ b/tests/Unity @@ -1 +1 @@ -Subproject commit 0e50926b52d8f27bfaa3b7c43e04c98f4f98ad21 +Subproject commit f23d8b25cd7e1fb89cb87e910ee2bed011d4ec2e diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index f83cef1d..a0e9336c 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -6,7 +6,7 @@ set(TARGET test_camera) file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.*c) set(APP_COMMON_LINK_LIBRARIES - tests::Unity + Unity camera::lib_camera ) @@ -41,7 +41,7 @@ add_executable(${TARGET} EXCLUDE_FROM_ALL) target_sources(${TARGET} PUBLIC ${APP_SOURCES}) target_include_directories(${TARGET} PUBLIC ${APP_INCLUDES}) target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) -target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) +target_compile_options(${TARGET} PUBLIC -g -Os -Wall -Wno-xcore-fptrgroup) target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) diff --git a/tests/unity.cmake b/tests/unity.cmake index 14c95614..14f3567d 100644 --- a/tests/unity.cmake +++ b/tests/unity.cmake @@ -1,6 +1,6 @@ # ############################################################################## # Target name -set(LIB_NAME lib_Unity) +set(LIB_NAME Unity) set(LIB_PATH ${CMAKE_CURRENT_LIST_DIR}/Unity/src) # Source files @@ -24,4 +24,4 @@ target_sources(${LIB_NAME} ${SOURCES_ASM} ) -add_library(tests::Unity ALIAS ${LIB_NAME}) +add_library(Unity::framework ALIAS ${LIB_NAME}) \ No newline at end of file From 45732a9774cb6a216c94a27545ba08f0f612db7a Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Tue, 30 May 2023 12:33:47 +0100 Subject: [PATCH 019/306] cleaning cmake, deleting .xn file --- CMakeLists.txt | 9 +- examples/CMakeLists.txt | 8 ++ .../{simple_timing/src => }/config.xscope | 0 examples/examples.cmake | 14 --- .../{simple_timing.cmake => CMakeLists.txt} | 14 ++- examples/simple_timing/XCORE-AI-EXPLORER.xn | 109 ------------------ .../{take_picture.cmake => CMakeLists.txt} | 15 ++- examples/take_picture/XCORE-AI-EXPLORER.xn | 109 ------------------ examples/take_picture/src/config.xscope | 22 ---- ...e_picture_dynamic.cmake => CMakeLists.txt} | 13 ++- .../take_picture_dynamic/XCORE-AI-EXPLORER.xn | 109 ------------------ .../take_picture_dynamic/src/config.xscope | 22 ---- ...ture_dynamic_crop.cmake => CMakeLists.txt} | 13 ++- .../XCORE-AI-EXPLORER.xn | 109 ------------------ .../src/config.xscope | 22 ---- modules/CMakeLists.txt | 2 +- tests/CMakeLists.txt | 1 - tests/unit_tests/CMakeLists.txt | 6 +- tests/unit_tests/XCORE-AI-EXPLORER.xn | 109 ------------------ tests/unity.cmake | 8 +- 20 files changed, 55 insertions(+), 659 deletions(-) create mode 100644 examples/CMakeLists.txt rename examples/{simple_timing/src => }/config.xscope (100%) delete mode 100644 examples/examples.cmake rename examples/simple_timing/{simple_timing.cmake => CMakeLists.txt} (81%) delete mode 100644 examples/simple_timing/XCORE-AI-EXPLORER.xn rename examples/take_picture/{take_picture.cmake => CMakeLists.txt} (82%) delete mode 100644 examples/take_picture/XCORE-AI-EXPLORER.xn delete mode 100644 examples/take_picture/src/config.xscope rename examples/take_picture_dynamic/{take_picture_dynamic.cmake => CMakeLists.txt} (82%) delete mode 100644 examples/take_picture_dynamic/XCORE-AI-EXPLORER.xn delete mode 100644 examples/take_picture_dynamic/src/config.xscope rename examples/take_picture_dynamic_crop/{take_picture_dynamic_crop.cmake => CMakeLists.txt} (82%) delete mode 100644 examples/take_picture_dynamic_crop/XCORE-AI-EXPLORER.xn delete mode 100644 examples/take_picture_dynamic_crop/src/config.xscope delete mode 100644 tests/unit_tests/XCORE-AI-EXPLORER.xn diff --git a/CMakeLists.txt b/CMakeLists.txt index 471d173c..b4c3cd34 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,8 @@ cmake_minimum_required(VERSION 3.21) +## Set the target +set(XCORE_TARGET "XCORE-AI-EXPLORER") + ## Disable in-source build. if("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}") message(FATAL_ERROR "In-source build is not allowed! Please specify a build folder.\n\tex:cmake -B build") @@ -25,9 +28,7 @@ add_subdirectory(camera) ## Add tests add_subdirectory(tests) -## Add top level project targets -if(PROJECT_IS_TOP_LEVEL) - include(examples/examples.cmake) -endif() # top level +## Add examples +add_subdirectory(examples) endif() # xs3a diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 00000000..dde51673 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,8 @@ +# set xscope config file path +set(CONFIG_XSCOPE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) + +# add the examples +add_subdirectory(simple_timing) +add_subdirectory(take_picture) +add_subdirectory(take_picture_dynamic) +add_subdirectory(take_picture_dynamic_crop) diff --git a/examples/simple_timing/src/config.xscope b/examples/config.xscope similarity index 100% rename from examples/simple_timing/src/config.xscope rename to examples/config.xscope diff --git a/examples/examples.cmake b/examples/examples.cmake deleted file mode 100644 index a3240222..00000000 --- a/examples/examples.cmake +++ /dev/null @@ -1,14 +0,0 @@ - -# edit subfolder, put your examples here -#TODO replace by glob -set(EXAMPLES - simple_timing - take_picture - take_picture_dynamic - take_picture_dynamic_crop - ) - -# add all the examples that you need -foreach(EXAMPLE ${EXAMPLES}) - include(${CMAKE_CURRENT_LIST_DIR}/${EXAMPLE}/${EXAMPLE}.cmake) -endforeach(EXAMPLE) diff --git a/examples/simple_timing/simple_timing.cmake b/examples/simple_timing/CMakeLists.txt similarity index 81% rename from examples/simple_timing/simple_timing.cmake rename to examples/simple_timing/CMakeLists.txt index 665634c1..fdd110ad 100644 --- a/examples/simple_timing/simple_timing.cmake +++ b/examples/simple_timing/CMakeLists.txt @@ -21,9 +21,11 @@ set(APP_COMPILER_FLAGS -fxscope -mcmodel=large -Wno-xcore-fptrgroup - ${CMAKE_CURRENT_LIST_DIR}/src/config.xscope - ${CMAKE_CURRENT_LIST_DIR}/XCORE-AI-EXPLORER.xn + -target=${XCORE_TARGET} + ${CONFIG_XSCOPE_PATH}/config.xscope ) + + set(APP_COMPILE_DEFINITIONS configENABLE_DEBUG_PRINTF=1 PLATFORM_SUPPORTS_TILE_0=1 @@ -35,10 +37,10 @@ set(APP_COMPILE_DEFINITIONS XUD_CORE_CLOCK=600 ) -set(APP_LINK_OPTIONS - -report - ${CMAKE_CURRENT_LIST_DIR}/XCORE-AI-EXPLORER.xn - ${CMAKE_CURRENT_LIST_DIR}/src/config.xscope +set(APP_LINK_OPTIONS + "-report" + "-target=${XCORE_TARGET}" + "${CONFIG_XSCOPE_PATH}/config.xscope" ) # <--- Link libraries diff --git a/examples/simple_timing/XCORE-AI-EXPLORER.xn b/examples/simple_timing/XCORE-AI-EXPLORER.xn deleted file mode 100644 index b7d3cd93..00000000 --- a/examples/simple_timing/XCORE-AI-EXPLORER.xn +++ /dev/null @@ -1,109 +0,0 @@ - - - Board - xcore.ai Explorer Kit - - - tileref tile[2] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/take_picture/take_picture.cmake b/examples/take_picture/CMakeLists.txt similarity index 82% rename from examples/take_picture/take_picture.cmake rename to examples/take_picture/CMakeLists.txt index 560e551a..e19ab2e1 100644 --- a/examples/take_picture/take_picture.cmake +++ b/examples/take_picture/CMakeLists.txt @@ -21,9 +21,11 @@ set(APP_COMPILER_FLAGS -fxscope -mcmodel=large -Wno-xcore-fptrgroup - ${CMAKE_CURRENT_LIST_DIR}/src/config.xscope - ${CMAKE_CURRENT_LIST_DIR}/XCORE-AI-EXPLORER.xn + -target=${XCORE_TARGET} + ${CONFIG_XSCOPE_PATH}/config.xscope ) + + set(APP_COMPILE_DEFINITIONS configENABLE_DEBUG_PRINTF=1 PLATFORM_SUPPORTS_TILE_0=1 @@ -35,12 +37,13 @@ set(APP_COMPILE_DEFINITIONS XUD_CORE_CLOCK=600 ) -set(APP_LINK_OPTIONS - -report - ${CMAKE_CURRENT_LIST_DIR}/XCORE-AI-EXPLORER.xn - ${CMAKE_CURRENT_LIST_DIR}/src/config.xscope +set(APP_LINK_OPTIONS + "-report" + "-target=${XCORE_TARGET}" + "${CONFIG_XSCOPE_PATH}/config.xscope" ) + # <--- Link libraries set(APP_COMMON_LINK_LIBRARIES core::general diff --git a/examples/take_picture/XCORE-AI-EXPLORER.xn b/examples/take_picture/XCORE-AI-EXPLORER.xn deleted file mode 100644 index b7d3cd93..00000000 --- a/examples/take_picture/XCORE-AI-EXPLORER.xn +++ /dev/null @@ -1,109 +0,0 @@ - - - Board - xcore.ai Explorer Kit - - - tileref tile[2] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/take_picture/src/config.xscope b/examples/take_picture/src/config.xscope deleted file mode 100644 index 69236026..00000000 --- a/examples/take_picture/src/config.xscope +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/take_picture_dynamic/take_picture_dynamic.cmake b/examples/take_picture_dynamic/CMakeLists.txt similarity index 82% rename from examples/take_picture_dynamic/take_picture_dynamic.cmake rename to examples/take_picture_dynamic/CMakeLists.txt index 7b2dfcc8..4a123287 100644 --- a/examples/take_picture_dynamic/take_picture_dynamic.cmake +++ b/examples/take_picture_dynamic/CMakeLists.txt @@ -21,8 +21,8 @@ set(APP_COMPILER_FLAGS -fxscope -mcmodel=large -Wno-xcore-fptrgroup - ${CMAKE_CURRENT_LIST_DIR}/src/config.xscope - ${CMAKE_CURRENT_LIST_DIR}/XCORE-AI-EXPLORER.xn + -target=${XCORE_TARGET} + ${CONFIG_XSCOPE_PATH}/config.xscope ) set(APP_COMPILE_DEFINITIONS @@ -36,12 +36,13 @@ set(APP_COMPILE_DEFINITIONS XUD_CORE_CLOCK=600 ) -set(APP_LINK_OPTIONS - -report - ${CMAKE_CURRENT_LIST_DIR}/XCORE-AI-EXPLORER.xn - ${CMAKE_CURRENT_LIST_DIR}/src/config.xscope +set(APP_LINK_OPTIONS + "-report" + "-target=${XCORE_TARGET}" + "${CONFIG_XSCOPE_PATH}/config.xscope" ) + # <--- Link libraries set(APP_COMMON_LINK_LIBRARIES core::general diff --git a/examples/take_picture_dynamic/XCORE-AI-EXPLORER.xn b/examples/take_picture_dynamic/XCORE-AI-EXPLORER.xn deleted file mode 100644 index b7d3cd93..00000000 --- a/examples/take_picture_dynamic/XCORE-AI-EXPLORER.xn +++ /dev/null @@ -1,109 +0,0 @@ - - - Board - xcore.ai Explorer Kit - - - tileref tile[2] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/take_picture_dynamic/src/config.xscope b/examples/take_picture_dynamic/src/config.xscope deleted file mode 100644 index 69236026..00000000 --- a/examples/take_picture_dynamic/src/config.xscope +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/take_picture_dynamic_crop/take_picture_dynamic_crop.cmake b/examples/take_picture_dynamic_crop/CMakeLists.txt similarity index 82% rename from examples/take_picture_dynamic_crop/take_picture_dynamic_crop.cmake rename to examples/take_picture_dynamic_crop/CMakeLists.txt index 8da2767d..f7672807 100644 --- a/examples/take_picture_dynamic_crop/take_picture_dynamic_crop.cmake +++ b/examples/take_picture_dynamic_crop/CMakeLists.txt @@ -21,9 +21,10 @@ set(APP_COMPILER_FLAGS -fxscope -mcmodel=large -Wno-xcore-fptrgroup - ${CMAKE_CURRENT_LIST_DIR}/src/config.xscope - ${CMAKE_CURRENT_LIST_DIR}/XCORE-AI-EXPLORER.xn + -target=${XCORE_TARGET} + ${CONFIG_XSCOPE_PATH}/config.xscope ) + set(APP_COMPILE_DEFINITIONS configENABLE_DEBUG_PRINTF=1 PLATFORM_SUPPORTS_TILE_0=1 @@ -35,10 +36,10 @@ set(APP_COMPILE_DEFINITIONS XUD_CORE_CLOCK=600 ) -set(APP_LINK_OPTIONS - -report - ${CMAKE_CURRENT_LIST_DIR}/XCORE-AI-EXPLORER.xn - ${CMAKE_CURRENT_LIST_DIR}/src/config.xscope +set(APP_LINK_OPTIONS + "-report" + "-target=${XCORE_TARGET}" + "${CONFIG_XSCOPE_PATH}/config.xscope" ) # <--- Link libraries diff --git a/examples/take_picture_dynamic_crop/XCORE-AI-EXPLORER.xn b/examples/take_picture_dynamic_crop/XCORE-AI-EXPLORER.xn deleted file mode 100644 index b7d3cd93..00000000 --- a/examples/take_picture_dynamic_crop/XCORE-AI-EXPLORER.xn +++ /dev/null @@ -1,109 +0,0 @@ - - - Board - xcore.ai Explorer Kit - - - tileref tile[2] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/take_picture_dynamic_crop/src/config.xscope b/examples/take_picture_dynamic_crop/src/config.xscope deleted file mode 100644 index 69236026..00000000 --- a/examples/take_picture_dynamic_crop/src/config.xscope +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index 33dff2fa..150e8177 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -1,3 +1,3 @@ add_subdirectory(core) -add_subdirectory(mipi) add_subdirectory(i2c) +add_subdirectory(mipi) \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b8b110d8..8e6aee61 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,3 +1,2 @@ add_subdirectory(unit_tests) -#add_subdirectory(Unity) include(unity.cmake) # custom cmake file diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index a0e9336c..9ad78598 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -13,8 +13,9 @@ set(APP_COMMON_LINK_LIBRARIES # ############################################################################## # Flags # ############################################################################## -set(APP_LINK_OPTIONS -report - ${CMAKE_CURRENT_LIST_DIR}/XCORE-AI-EXPLORER.xn +set(APP_LINK_OPTIONS + "-report" + "-target=${XCORE_TARGET}" ${CMAKE_CURRENT_LIST_DIR}/config.xscope ) @@ -26,7 +27,6 @@ set(APP_COMPILER_FLAGS -mcmodel=large -Wno-xcore-fptrgroup ${CMAKE_CURRENT_LIST_DIR}/config.xscope - ${CMAKE_CURRENT_LIST_DIR}/XCORE-AI-EXPLORER.xn ) set(APP_COMPILE_DEFINITIONS diff --git a/tests/unit_tests/XCORE-AI-EXPLORER.xn b/tests/unit_tests/XCORE-AI-EXPLORER.xn deleted file mode 100644 index b7d3cd93..00000000 --- a/tests/unit_tests/XCORE-AI-EXPLORER.xn +++ /dev/null @@ -1,109 +0,0 @@ - - - Board - xcore.ai Explorer Kit - - - tileref tile[2] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/unity.cmake b/tests/unity.cmake index 14f3567d..0f57361c 100644 --- a/tests/unity.cmake +++ b/tests/unity.cmake @@ -13,7 +13,13 @@ add_library(${LIB_NAME} STATIC) target_include_directories( ${LIB_NAME} PUBLIC - ${CMAKE_CURRENT_LIST_DIR}/Unity/src/ + ${LIB_PATH} +) + +target_link_options( + ${LIB_NAME} + PRIVATE + "-target=${XCORE_TARGET}" ) target_sources(${LIB_NAME} From b8315f4ed36724a53ea065b1894d8ae84fcaa2b7 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Tue, 30 May 2023 12:41:08 +0100 Subject: [PATCH 020/306] cleaning cmake from examples --- modules/i2c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/i2c b/modules/i2c index 776d465d..05952714 160000 --- a/modules/i2c +++ b/modules/i2c @@ -1 +1 @@ -Subproject commit 776d465d604783f2a7072b3d406466d184d2fa21 +Subproject commit 05952714dc0092f87f6610098ad3bbaea8b277fb From b162ec56e31ff76c32d73aad8960eecec0b0ad9a Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Tue, 30 May 2023 14:40:21 +0100 Subject: [PATCH 021/306] adding newline at the end of file --- camera/asm/rgb_to_yuv.S | 2 +- tests/unit_tests/CMakeLists.txt | 2 +- tests/unit_tests/src/test_isp.c | 2 +- tests/unit_tests/src/test_isp.h | 2 +- tests/unity.cmake | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/camera/asm/rgb_to_yuv.S b/camera/asm/rgb_to_yuv.S index a623317d..131f1931 100644 --- a/camera/asm/rgb_to_yuv.S +++ b/camera/asm/rgb_to_yuv.S @@ -65,4 +65,4 @@ Yconv: .word 77, 150, 29, 0, 0,0,0,0 // https://en.wikipedia.org/wiki/YUV // https://softpixel.com/~cwright/programming/colorspace/yuv/ // https://www.mikekohn.net/file_formats/yuv_rgb_converter.php -// \ No newline at end of file +// diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index 9ad78598..1a8e7499 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -47,4 +47,4 @@ target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) # enable testing functionality -add_test(NAME ${TARGET} COMMAND ${TARGET}) \ No newline at end of file +add_test(NAME ${TARGET} COMMAND ${TARGET}) diff --git a/tests/unit_tests/src/test_isp.c b/tests/unit_tests/src/test_isp.c index 336c3de7..658ce5d1 100644 --- a/tests/unit_tests/src/test_isp.c +++ b/tests/unit_tests/src/test_isp.c @@ -59,4 +59,4 @@ int main(void) RUN_TEST(test_rgb_to_yuv); RUN_TEST(test_yuv_to_rgb); return UNITY_END(); -} \ No newline at end of file +} diff --git a/tests/unit_tests/src/test_isp.h b/tests/unit_tests/src/test_isp.h index 3d1b61bd..afbe8c10 100644 --- a/tests/unit_tests/src/test_isp.h +++ b/tests/unit_tests/src/test_isp.h @@ -31,4 +31,4 @@ void rgb_to_yuv_ct(color_table_t* ct_ref, color_table_t* ct_res){ ct_res -> Y = (uint8_t)(GET_Y(result) + CT_INT); ct_res -> U = (uint8_t)(GET_U(result) + CT_INT); ct_res -> V = (uint8_t)(GET_V(result) + CT_INT); -} \ No newline at end of file +} diff --git a/tests/unity.cmake b/tests/unity.cmake index 0f57361c..284c27a9 100644 --- a/tests/unity.cmake +++ b/tests/unity.cmake @@ -30,4 +30,4 @@ target_sources(${LIB_NAME} ${SOURCES_ASM} ) -add_library(Unity::framework ALIAS ${LIB_NAME}) \ No newline at end of file +add_library(Unity::framework ALIAS ${LIB_NAME}) From 97c11420c5707522d9b0d5b1dad90c7d5b276bf7 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Tue, 30 May 2023 15:06:25 +0100 Subject: [PATCH 022/306] deleting target_link_options and linking back origin repo --- tests/unity.cmake | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/unity.cmake b/tests/unity.cmake index 284c27a9..8c3b5af0 100644 --- a/tests/unity.cmake +++ b/tests/unity.cmake @@ -16,12 +16,6 @@ target_include_directories( ${LIB_PATH} ) -target_link_options( - ${LIB_NAME} - PRIVATE - "-target=${XCORE_TARGET}" -) - target_sources(${LIB_NAME} PRIVATE ${SOURCES_C} From 09a5f47601c89269ced698545113523cf39426d4 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Tue, 30 May 2023 15:08:53 +0100 Subject: [PATCH 023/306] restoring i2c --- modules/i2c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/i2c b/modules/i2c index 05952714..776d465d 160000 --- a/modules/i2c +++ b/modules/i2c @@ -1 +1 @@ -Subproject commit 05952714dc0092f87f6610098ad3bbaea8b277fb +Subproject commit 776d465d604783f2a7072b3d406466d184d2fa21 From bb006c9c3e6fcf55782fd3799989b60af28c244e Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Tue, 30 May 2023 15:23:26 +0100 Subject: [PATCH 024/306] clean comments --- tests/unit_tests/src/test_isp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/src/test_isp.c b/tests/unit_tests/src/test_isp.c index 658ce5d1..befea534 100644 --- a/tests/unit_tests/src/test_isp.c +++ b/tests/unit_tests/src/test_isp.c @@ -7,12 +7,12 @@ color_table_t ct_test = {16, 100, 16, 65, 100, 92}; // R G B Y U V void setUp(void) { - // set stuff up here + } void tearDown(void) { - // clean stuff up here + } void test_yuv_to_rgb() From e815e7b501800d6e78dbe56a3c5f7ccc48b4785e Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Thu, 1 Jun 2023 16:09:18 +0100 Subject: [PATCH 025/306] removing mipi submodule, adding mipi parts --- .gitmodules | 4 ---- examples/simple_timing/src/mipi_timing.h | 4 ---- examples/simple_timing/src/mipi_timing.xc | 3 +-- examples/take_picture/src/mipi_main.h | 4 ---- examples/take_picture/src/mipi_main.xc | 3 +-- examples/take_picture_dynamic/src/mipi_main.h | 4 ---- examples/take_picture_dynamic/src/mipi_main.xc | 3 +-- examples/take_picture_dynamic_crop/src/mipi_main.h | 4 ---- examples/take_picture_dynamic_crop/src/mipi_main.xc | 3 +-- launch_cmake.sh | 0 modules/CMakeLists.txt | 2 +- modules/mipi | 1 - 12 files changed, 5 insertions(+), 30 deletions(-) mode change 100644 => 100755 launch_cmake.sh delete mode 160000 modules/mipi diff --git a/.gitmodules b/.gitmodules index fa02534c..0408c2c4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,10 +6,6 @@ path = modules/i2c url = git@github.com:xmos/lib_i2c # adding lib assert and cmake to the repo branch = develop -[submodule "modules/mipi"] - path = modules/mipi - url = git@github.com:xalbertoisorna/lib_mipi # same adding cmake to the repo - branch = master [submodule "modules/core"] path = modules/core url = git@github.com:xmos/fwk_core # xcore math inside the repo diff --git a/examples/simple_timing/src/mipi_timing.h b/examples/simple_timing/src/mipi_timing.h index 4b02d074..4e51d250 100644 --- a/examples/simple_timing/src/mipi_timing.h +++ b/examples/simple_timing/src/mipi_timing.h @@ -28,12 +28,8 @@ typedef struct unsigned line_number; } image_rx_t; - static mipi_packet_t packet_buffer[MIPI_PKT_BUFFER_COUNT]; -// Functions definitions -#define gMipiPacketRx(...) MipiPacketRxnrev(__VA_ARGS__) // define your own function to use - #ifdef __XC__ void mipi_main(client interface i2c_master_if i2c); diff --git a/examples/simple_timing/src/mipi_timing.xc b/examples/simple_timing/src/mipi_timing.xc index 266ca648..0add1fc0 100644 --- a/examples/simple_timing/src/mipi_timing.xc +++ b/examples/simple_timing/src/mipi_timing.xc @@ -26,7 +26,6 @@ // MIPI #include "mipi_timing.h" -#include "MipiPacket.h" // Sensor #define MSG_SUCCESS "Stream start OK\n" @@ -293,7 +292,7 @@ void mipi_main(client interface i2c_master_if i2c) // start the different jobs (packet controller, handler, and post_process) par { - gMipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); + MipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); mipi_packet_handler(c_pkt, c_ctrl, flag); save_image_to_file(flag); } diff --git a/examples/take_picture/src/mipi_main.h b/examples/take_picture/src/mipi_main.h index 089a1a14..1ab51876 100644 --- a/examples/take_picture/src/mipi_main.h +++ b/examples/take_picture/src/mipi_main.h @@ -29,12 +29,8 @@ typedef struct unsigned line_number; } image_rx_t; - static mipi_packet_t packet_buffer[MIPI_PKT_BUFFER_COUNT]; -// Functions definitions -#define gMipiPacketRx(...) MipiPacketRxnrev(__VA_ARGS__) // define your own function to use - #ifdef __XC__ void mipi_main(client interface i2c_master_if i2c); #endif diff --git a/examples/take_picture/src/mipi_main.xc b/examples/take_picture/src/mipi_main.xc index 99bce5ad..fac21aac 100644 --- a/examples/take_picture/src/mipi_main.xc +++ b/examples/take_picture/src/mipi_main.xc @@ -15,7 +15,6 @@ // MIPI #include "mipi_main.h" -#include "MipiPacket.h" // Sensor #define MSG_SUCCESS "Stream start OK\n" @@ -224,7 +223,7 @@ void mipi_main(client interface i2c_master_if i2c) // start the different jobs (packet controller, handler, and post_process) par { - gMipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); + MipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); mipi_packet_handler(c_pkt, c_ctrl, flag); save_image_to_file(flag); } diff --git a/examples/take_picture_dynamic/src/mipi_main.h b/examples/take_picture_dynamic/src/mipi_main.h index 34cc0926..788c849b 100644 --- a/examples/take_picture_dynamic/src/mipi_main.h +++ b/examples/take_picture_dynamic/src/mipi_main.h @@ -40,12 +40,8 @@ typedef struct unsigned line_number; } image_rx_t; - static mipi_packet_t packet_buffer[MIPI_PKT_BUFFER_COUNT]; -// Functions definitions -#define gMipiPacketRx(...) MipiPacketRxnrev(__VA_ARGS__) // define your own function to use - #ifdef __XC__ void mipi_main(client interface i2c_master_if i2c); diff --git a/examples/take_picture_dynamic/src/mipi_main.xc b/examples/take_picture_dynamic/src/mipi_main.xc index a630ff7c..6f928087 100644 --- a/examples/take_picture_dynamic/src/mipi_main.xc +++ b/examples/take_picture_dynamic/src/mipi_main.xc @@ -15,7 +15,6 @@ // MIPI #include "mipi_main.h" -#include "MipiPacket.h" // Sensor #define MSG_SUCCESS "Stream start OK\n" @@ -228,7 +227,7 @@ void mipi_main(client interface i2c_master_if i2c) // start the different jobs (packet controller, handler, and post_process) par { - gMipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); + MipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); mipi_packet_handler(c_pkt, c_ctrl, flag, img_raw_ptr, ch_exp); //save_image_to_file(flag, img_raw_ptr); control_exposure(ch_exp, i2c); diff --git a/examples/take_picture_dynamic_crop/src/mipi_main.h b/examples/take_picture_dynamic_crop/src/mipi_main.h index 34cc0926..788c849b 100644 --- a/examples/take_picture_dynamic_crop/src/mipi_main.h +++ b/examples/take_picture_dynamic_crop/src/mipi_main.h @@ -40,12 +40,8 @@ typedef struct unsigned line_number; } image_rx_t; - static mipi_packet_t packet_buffer[MIPI_PKT_BUFFER_COUNT]; -// Functions definitions -#define gMipiPacketRx(...) MipiPacketRxnrev(__VA_ARGS__) // define your own function to use - #ifdef __XC__ void mipi_main(client interface i2c_master_if i2c); diff --git a/examples/take_picture_dynamic_crop/src/mipi_main.xc b/examples/take_picture_dynamic_crop/src/mipi_main.xc index 0d239f9c..34ab1fe9 100644 --- a/examples/take_picture_dynamic_crop/src/mipi_main.xc +++ b/examples/take_picture_dynamic_crop/src/mipi_main.xc @@ -15,7 +15,6 @@ // MIPI #include "mipi_main.h" -#include "MipiPacket.h" // Sensor #define MSG_SUCCESS "Stream start OK\n" @@ -233,7 +232,7 @@ void mipi_main(client interface i2c_master_if i2c) // start the different jobs (packet controller, handler, and post_process) par { - gMipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); + MipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); mipi_packet_handler(c_pkt, c_ctrl, img_raw_ptr, ch_exp); //save_image_to_file(flag, img_raw_ptr); control_exposure(ch_exp, i2c); diff --git a/launch_cmake.sh b/launch_cmake.sh old mode 100644 new mode 100755 diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index cf309c8c..b721e9d5 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -1,6 +1,6 @@ # adding submodules here add_subdirectory(core) -add_subdirectory(mipi) +add_subdirectory(mipi2) # lib_i2c and lib_xassert do not have CMakeLists.txt # so using wrappers here include(xassert.cmake) diff --git a/modules/mipi b/modules/mipi deleted file mode 160000 index d124e8e6..00000000 --- a/modules/mipi +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d124e8e6a3607273252a820023b1c1154e886337 From 32a72319329fbbbd5f0687b28cbdf0d0183f6e29 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Thu, 1 Jun 2023 16:09:49 +0100 Subject: [PATCH 026/306] adding missing directory --- modules/mipi2/CMakeLists.txt | 23 +++ modules/mipi2/README.md | 0 modules/mipi2/api/mipi.h | 57 ++++++ modules/mipi2/api/mipi_defines.h | 124 +++++++++++++ modules/mipi2/src/MipiPacketRx.S | 284 ++++++++++++++++++++++++++++++ modules/mipi2/src/MipiPacketRx.c | 65 +++++++ modules/mipi2/src/MipiPacketRx.xc | 46 +++++ 7 files changed, 599 insertions(+) create mode 100644 modules/mipi2/CMakeLists.txt create mode 100644 modules/mipi2/README.md create mode 100644 modules/mipi2/api/mipi.h create mode 100644 modules/mipi2/api/mipi_defines.h create mode 100644 modules/mipi2/src/MipiPacketRx.S create mode 100644 modules/mipi2/src/MipiPacketRx.c create mode 100644 modules/mipi2/src/MipiPacketRx.xc diff --git a/modules/mipi2/CMakeLists.txt b/modules/mipi2/CMakeLists.txt new file mode 100644 index 00000000..56a9624e --- /dev/null +++ b/modules/mipi2/CMakeLists.txt @@ -0,0 +1,23 @@ + +## Target name +set( LIB_NAME lib_mipi ) + +add_library(${LIB_NAME} STATIC) + +target_sources(${LIB_NAME} + PRIVATE + src/MipiPacketRx.S + src/MipiPacketRx.xc +) + +target_include_directories(${LIB_NAME} + PUBLIC + api +) + +target_compile_options(${LIB_NAME} + PRIVATE + -Os -g -Wall +) + +add_library(mipi::lib_mipi ALIAS ${LIB_NAME}) diff --git a/modules/mipi2/README.md b/modules/mipi2/README.md new file mode 100644 index 00000000..e69de29b diff --git a/modules/mipi2/api/mipi.h b/modules/mipi2/api/mipi.h new file mode 100644 index 00000000..3466e021 --- /dev/null +++ b/modules/mipi2/api/mipi.h @@ -0,0 +1,57 @@ + +#pragma once + +#include +#include + +#include "mipi_defines.h" + +#ifndef __XC__ +#include +#include +#include +#endif + +#ifdef __XC__ + +void MipiPacketRx_init( + tileref tile, + buffered in port:32 p_mipi_rxd, + in port p_mipi_rxv, + in port p_mipi_rxa, + in port p_mipi_clk, + clock clk_mipi, + uint32_t demuxEn, + uint32_t dataType, + xMIPI_DemuxMode_t demuxMode, + uint32_t mipiClkDiv, + uint32_t cfgClkDiv); + +void MipiPacketRx( + buffered in port:32 p_mipi_rxd, + in port p_mipi_rxa, + streaming chanend c_pkt, + streaming chanend c_ctrl); + +#else + +void MipiPacketRx_init( + unsigned tile, + port_t p_mipi_rxd, + port_t p_mipi_rxv, + port_t p_mipi_rxa, + port_t p_mipi_clk, + xclock_t clk_mipi, + uint32_t demuxEn, + uint32_t dataType, + xMIPI_DemuxMode_t demuxMode, + uint32_t mipiClkDiv, + uint32_t cfgClkDiv); + +void MipiPacketRx( + port_t p_mipi_rxd, + port_t p_mipi_rxa, + chanend_t c_pkt, + chanend_t c_ctrl); + +#endif diff --git a/modules/mipi2/api/mipi_defines.h b/modules/mipi2/api/mipi_defines.h new file mode 100644 index 00000000..a74ce7d4 --- /dev/null +++ b/modules/mipi2/api/mipi_defines.h @@ -0,0 +1,124 @@ + +#pragma once + +typedef unsigned mipi_header_t; + +#define MIPI_HAS_PACKET_ERROR(STATUS) ((STATUS) & (0xFE)) + +#define MIPI_LONG_PACKET_MASK (0x30) +#define MIPI_IS_LONG_PACKET(HEADER) ((HEADER) & (MIPI_LONG_PACKET_MASK)) + +#define MIPI_DATA_TYPE_MASK (0x0000003F) +#define MIPI_GET_DATA_TYPE(HEADER) ((HEADER) & (MIPI_DATA_TYPE_MASK)) + +#define MIPI_GET_WORD_COUNT(HEADER) ( ((HEADER) >> 8) & 0xFFFF ) + +/** + * 0x00 to 0x07 - Synchronization Short Packet Data Types + * 0x08 to 0x0F - Generic Short Packet Data Types + * 0x10 to 0x17 - Generic Long Packet Data Types + * 0x18 to 0x1F - YUV Data + * 0x20 to 0x26 - RGB Data + * 0x27 to 0x2F - RAW Data + * 0x30 to 0x37 - User Defined Byte-based Data + * 0x38 - USL Commands + * 0x39 to 0x3E - Reserved for future use + * 0x3F - Unavailable (0x3F is used for LRTE EPD Spacer) + * + */ + +typedef enum { + // 0x00 to 0x07 - Synchronization Short Packet Data Types + MIPI_DT_FRAME_START = 0x00, + MIPI_DT_FRAME_END = 0x01, + MIPI_DT_LINE_START = 0x02, + MIPI_DT_LINE_END = 0x03, + MIPI_DT_EOT = 0x04, + // MIPI_DT_RESERVED_0x05 = 0x05, + // MIPI_DT_RESERVED_0x06 = 0x06, + // MIPI_DT_RESERVED_0x07 = 0x07, + + // 0x08 to 0x0F - Generic Short Packet Data Types + MIPI_DT_GENERIC_SHORT1 = 0x08, + MIPI_DT_GENERIC_SHORT2 = 0x09, + MIPI_DT_GENERIC_SHORT3 = 0x0A, + MIPI_DT_GENERIC_SHORT4 = 0x0B, + MIPI_DT_GENERIC_SHORT5 = 0x0C, + MIPI_DT_GENERIC_SHORT6 = 0x0D, + MIPI_DT_GENERIC_SHORT7 = 0x0E, + MIPI_DT_GENERIC_SHORT8 = 0x0F, + + // 0x10 to 0x17 - Generic Long Packet Data Types + MIPI_DT_NULL = 0x10, + MIPI_DT_BLANKING_DATA = 0x11, + MIPI_DT_8BIT_NONIMAGE = 0x12, + MIPI_DT_GENERIC_LONG1 = 0x13, + MIPI_DT_GENERIC_LONG2 = 0x14, + MIPI_DT_GENERIC_LONG3 = 0x15, + MIPI_DT_GENERIC_LONG4 = 0x16, + MIPI_DT_RESERVED_0x17 = 0x17, + + // 0x18 to 0x1F - YUV Data + MIPI_DT_YUV420_8BIT = 0x18, + MIPI_DT_YUV420_10BIT = 0x19, + MIPI_DT_YUV420_8BIT_LEGACY = 0x1A, + MIPI_DT_YUV420_8BIT_CHROMA_SHIFTED = 0x1C, + MIPI_DT_YUV420_10BIT_CHROMA_SHIFTED = 0x1D, + MIPI_DT_YUV422_8BIT = 0x1E, + MIPI_DT_YUV422_10BIT = 0x1F, + + // 0x20 to 0x26 - RGB Data + MIPI_DT_RGB444 = 0x20, + MIPI_DT_RGB555 = 0x21, + MIPI_DT_RGB565 = 0x22, + MIPI_DT_RGB666 = 0x23, + MIPI_DT_RGB888 = 0x24, + // MIPI_DT_RESERVED_0x25 = 0x25, + // MIPI_DT_RESERVED_0x26 = 0x26, + + // 0x27 to 0x2F - RAW Data + MIPI_DT_RAW24 = 0x27, + MIPI_DT_RAW6 = 0x28, + MIPI_DT_RAW7 = 0x29, + MIPI_DT_RAW8 = 0x2A, + MIPI_DT_RAW10 = 0x2B, + MIPI_DT_RAW12 = 0x2C, + MIPI_DT_RAW14 = 0x2D, + MIPI_DT_RAW16 = 0x2E, + MIPI_DT_RAW20 = 0x2F, + + // 0x30 to 0x37 - User Defined Byte-based Data + MIPI_DT_USER_DEFINED1 = 0x30, + MIPI_DT_USER_DEFINED2 = 0x31, + MIPI_DT_USER_DEFINED3 = 0x32, + MIPI_DT_USER_DEFINED4 = 0x33, + MIPI_DT_USER_DEFINED5 = 0x34, + MIPI_DT_USER_DEFINED6 = 0x35, + MIPI_DT_USER_DEFINED7 = 0x36, + MIPI_DT_USER_DEFINED8 = 0x37, + + // 0x38 - USL Commands + MIPI_DT_USL = 0x38, + + // 0x39 to 0x3E - Reserved for future use + // MIPI_DT_RESERVED_0x39 = 0x39, + // MIPI_DT_RESERVED_0x3A = 0x3A, + // MIPI_DT_RESERVED_0x3B = 0x3B, + // MIPI_DT_RESERVED_0x3C = 0x3C, + // MIPI_DT_RESERVED_0x3D = 0x3D, + // MIPI_DT_RESERVED_0x3E = 0x3E, + + // 0x3F - Unavailable (0x3F is used for LRTE EPD Spacer) + +} mipi_data_type_t; + +typedef enum xMIPI_DemuxMode_t { + XMIPI_DEMUXMODE_RESERVED = 0, + XMIPI_DEMUXMODE_10TO16 = 1, + XMIPI_DEMUXMODE_12TO16 = 2, + XMIPI_DEMUXMODE_14TO16 = 3, + XMIPI_DEMUXMODE_565TO88 = 4, + XMIPI_DEMUXMODE_8TO10 = 5, + XMIPI_DEMUXMODE_12TO8 = 6, + XMIPI_DEMUXMODE_14TO8 = 7 +} xMIPI_DemuxMode_t; diff --git a/modules/mipi2/src/MipiPacketRx.S b/modules/mipi2/src/MipiPacketRx.S new file mode 100644 index 00000000..beceaa20 --- /dev/null +++ b/modules/mipi2/src/MipiPacketRx.S @@ -0,0 +1,284 @@ + +#include +#include + + +/**************************************************** + **************************************************** + +Then to receive a packet: + +- Load context data from global memory (RxA, RxD) +- If RxA is high, wait for it to go low. (so we don't start mid-packet) + - Initial version can just do this with polling so we don't need to do any + weird jiggery pokery with the RxA low event vector (which we're already + using for a different purpose). +- Once RxA is low, use IN to wait for the first word of the packet +- Put the first word in the buffer +- If it's a short packet, return + - Maybe it's a good idea to return the data type? +- Otherwse, do the data loop +- Return + - Maybe it's a good idea to return the data type? + + **************************************************** + **************************************************** + */ + +/* + See MIPI CSI section 9.1.2 + + typedef struct { + uint8_t data_id; + uint16_t word_count; // In MIPI a word is a byte + uint8_t vcx_ecc; + } mipi_packet_header_t; + + Note: When that 32-bit header is read in on the buffered port, the data_id + will be the *least* significant 8 bits because of the direction of this port's + shift register. + + Bits: + data_id[7:6] - least significant two bits of 4-bit Virtual Channel ID + data_id[5:0] - data_type + vcx_ecc[7:6] - most significant two bits of 4-bit Virtual Channel ID + vcx_ecc[5:0] - error correction code +*/ + +.issue_mode dual + +#define FUNCTION_NAME MipiPacketRx +#define NSTACKWORDS 8 + +.globl FUNCTION_NAME.nstackwords +.globl FUNCTION_NAME.maxthreads +.globl FUNCTION_NAME.maxtimers +.globl FUNCTION_NAME.maxchanends + +.linkset FUNCTION_NAME.nstackwords, NSTACKWORDS +.linkset FUNCTION_NAME.maxchanends, 0 +.linkset FUNCTION_NAME.maxtimers, 0 +.linkset FUNCTION_NAME.maxthreads, 0 + +.globl FUNCTION_NAME +.type FUNCTION_NAME, @function +.text +.cc_top FUNCTION_NAME.func, FUNCTION_NAME + +#define P_RXD r0 +#define P_RXA r1 +#define C_PKT r2 +#define BUFF r3 + +#define HEADER r4 +#define S0 r5 +#define S1 r6 +#define BUFF_HEAD r7 + + +/**************************************************** + **************************************************** + + extern struct { + uint32_t RxD; + uint32_t RxA; + } mipi_context; + + Non-zero return signals error. + + unsigned MipiPacketRx(uint8_t packet_buffer[]); + + void MipiPacketRx2( + const unsigned RxD, + const unsigned RxA, + streaming chanend c_pkt, + streaming chanend c_ctrl) + + **************************************************** + **************************************************** +*/ +.align 4 +.skip 0 +FUNCTION_NAME: + dualentsp NSTACKWORDS + std r4, r5, sp[1] + std r6, r7, sp[2] + std r8, r9, sp[3] + + +// Enable MIPI shim hardware +{ ldc r4, 0x01 ; ldc r9, 8 } +{ shl r4, r4, r9 ; } + ldc r9, XS1_PS_XCORE_CTRL0 + get r11, ps[r9] +{ or r4, r4, r11 ; } + set ps[r9], r4 + + +// Enable RxA event + setc res[P_RXA], XS1_SETC_COND_EQ // <-- event on edge of RxA + setc res[P_RXA], XS1_SETC_IE_MODE_EVENT // <-- do event, not interrupt + ldc r11, 0 + setd res[P_RXA], r11 // <-- falling edge + ldap r11, .L_MIPI_RXA_LOW + setv res[P_RXA], r11 + +// Enable events for the thread + setsr 1 // Sets the EEBLE bit (I couldn't find a #define!) + +// Set up interrupt for c_ctrl +// Note: has to be interrupt because we can't have events always enabled. +// Wait, no, events can be enabled/disabled per resource. I should be able to +// use an event... +/* +//////// TODO /////// +*/ + + +#if 0 // astew: I'm not sure if we need this, since we just turned on the shim + // but I don't see why we couldn't start receiving a packet while + // setting up the RxA event or whatever. + + +// Check whether the RxA signal is currently high. +// If so, we need to wait until it is low or we will start reading mid-packet + +.L_WAIT_LOOP: +{ peek S0, res[P_RXA] ; } +{ ecallt S0 ; } +{ ; bt S0, .L_WAIT_LOOP } + +#endif + +.L_GET_NEXT_BUFFER: + +{ in BUFF, res[C_PKT] ; } +{ mov BUFF_HEAD, BUFF ; bf BUFF, .L_EXIT_THREAD } // <-- exit if null buffer + +.L_WAIT_FOR_NEXT_PACKET: + +// peek S0, res[P_RXA] +// ecallt S0 + +// Receive packet header from data port +{ in HEADER, res[P_RXD] ; ldc S1, 0x00000030 } +{ and S1, S1, HEADER ; stw HEADER, BUFF[0] } + +// Check whether the header indicates it is a long or short packet. +// If long, enable the falling-edge event on RxA. +// If short, we already have the whole packet +{ eet S1, res[P_RXA] ; bt S1, .L_RX_LONG } + + +.L_RX_SHORT: +// A short packet was received. +{ out res[C_PKT], BUFF_HEAD ; bu .L_GET_NEXT_BUFFER } + + +.L_RX_LONG: +// Long packet incoming. The header has been placed into the buffer, but hold on +// to it until we get the rest of the packet. + +{ in S0, res[P_RXD] ; add BUFF, BUFF, 4 } +.L_RX_LONG_LOOP: + // { byterev S0, S0 ; } + { add BUFF, BUFF, 4 ; stw S0, BUFF[0] } + { in S0, res[P_RXD] ; bu .L_RX_LONG_LOOP } + + /**** + * RxA event vector target. + * + * RxA is a signal coming from the xcore.ai hardware's MIPI shim layer, + * signaling high while a packet is being received. On the falling edge of the + * signal, the packet has been fully received, and this event fires, breaking + * the thread out of the receive loop. + * + * If the received packet was not a multiple of 4 bytes, there will still be + * data left in the shift register. This event grabs any remaining data and puts + * it at the end of the received frame. + * + * When the data has been grabbed, it jumps down to .L_RxLONG_DataEnd + * + * This is only used for LONG frames. Short frames are always exactly 32 bits. + ***** + */ +.align 32 +.skip 20 +.L_MIPI_RXA_LOW: + +// Clear event on RxA port +{ in r11, res[P_RXA] ; } // <-- aligned 20 mod 32 +// End input on P_RXD, so we can pull out any remaining bytes without blocking. +{ endin r11, res[P_RXD] ; } // <-- aligned 24 mod 32 + +// NOTE: r11 will contain a number of BITS remaining, instead of bytes. The +// .align 32's below just avoid the need to change it to dual-issue instructions +// for the BRU instruction. r11 will be one of {0,8,16,24} +{ in S1, res[P_RXD] ; bru r11 } // <-- aligned 28 mod 32 + + +// In each of the 4 cases, the very last byte of the transmission should end +// up in S2. The last byte contains error bits and whatnot + +// In the 0 case, the byte with the error details is already in S2, but in the +// most significant bits, so pull it down. +//.... is it already in S2..? If r11 is zero, what did the IN instruction paired +// with BRU do? I suspect this is wrong.. +// +.L_RXA_OUT_TAIL0: // (0 byte tail) +{ shr S0, S0, 24 ; } // <-- aligned 0 mod 32 + bu .L_RXA_DATA_END + +.align 32 +.L_RXA_OUT_TAIL1: // (1 Tail Byte; the status byte) +{ shr S0, S1, 24 ; } +{ ; stw S0, BUFF[0] } +{ ; bu .L_RXA_DATA_END } + +.align 32 +.L_RXA_OUT_TAIL2: // (2 Tail Bytes) +{ shr S1, S1, 16 ; } +{ shr S0, S1, 8 ; stw S1, BUFF[0] } +{ ; bu .L_RXA_DATA_END } + +.align 32 +.L_RXA_OUT_TAIL3: // (3 Tail Bytes) +{ shr S1, S1, 8 ; } +{ shr S0, S1, 16 ; stw S1, BUFF[0] } +{ ; bu .L_RXA_DATA_END } + +.align 8 +.L_RXA_DATA_END: + +// We've received the full packet and S0 contains the status byte + +// Disable the RxA event +// (any bits set in S2 other than LSb indicate an error) +{ edu res[P_RXA] ; shr S0, S0, 1 } + +// Here's how we'll deal with received packet errors for now: If there is a +// packet error (if S0 is nonzero right here) in this long packet, change the +// data length in the packet header to 0. The header is already in the buffer, +// so if the next layer really needs to know what we saw for a packet length, +// it can just look in the buffer. +// +// Actually, for now, just assert on any errors. +{ out res[C_PKT], BUFF_HEAD ; ecallt S0 } + +// And now we need a new buffer +// Also reenable events. +{ setsr 1 ; bu .L_GET_NEXT_BUFFER } + + +.L_EXIT_THREAD: + + ldd r8, r9, sp[3] + ldd r6, r7, sp[2] + ldd r4, r5, sp[1] + retsp NSTACKWORDS + +.size FUNCTION_NAME, .-FUNCTION_NAME +.cc_bottom FUNCTION_NAME.func + + + diff --git a/modules/mipi2/src/MipiPacketRx.c b/modules/mipi2/src/MipiPacketRx.c new file mode 100644 index 00000000..793c7dea --- /dev/null +++ b/modules/mipi2/src/MipiPacketRx.c @@ -0,0 +1,65 @@ + + +#include +#include +#include + +#include "mipi.h" + +#define SHIFT 0x3 +#define DELAY_SIZE 0x9 +#define DELAY_MASK (((1 << DELAY_SIZE) - 1) << SHIFT) +#define DELAY(x) (((x) & DELAY_MASK) >> SHIFT) +#define DELAY_SET(x, v) (((x) & ~DELAY_MASK) | (((v) << SHIFT) & DELAY_MASK)) + +void MipiPacketRx_init( + unsigned tile, + port_t p_mipi_rxd, + port_t p_mipi_rxv, + port_t p_mipi_rxa, + port_t p_mipi_clk, + xclock_t clk_mipi, + uint32_t demuxEn, + uint32_t dataType, + xMIPI_DemuxMode_t demuxMode, + uint32_t mipiClkDiv, + uint32_t cfgClkDiv) +{ + //configure_in_port_strobed_slave(p_mipi_rxd, p_mipi_rxv, clk_mipi); + port_protocol_in_strobed_slave(p_mipi_rxd, p_mipi_rxv, clk_mipi); + + //set_clock_src(clk_mipi, p_mipi_clk); + clock_set_ready_src(clk_mipi, p_mipi_clk); + + /* Sample on falling edge - shim outputting on rising */ + //set_clock_rise_delay(clk_mipi, 1); + __xcore_resource_setc(clk_mipi, DELAY_SET(0x9007, 1)); + + //set_pad_delay(p_mipi_rxa, 1); + __xcore_resource_setc(p_mipi_rxa, DELAY_SET(0x7007, 1)); + + //start_clock(clk_mipi); + clock_start(clk_mipi); + + // take DPHY out of reset + + //write_node_config_reg(tile, XS1_SSWITCH_MIPI_DPHY_CFG0_NUM, 3); + write_sswitch_reg(tile, XS1_SSWITCH_MIPI_DPHY_CFG0_NUM, 3); + + // set clock dividers to create suitable frequency + + //write_node_config_reg(tile, XS1_SSWITCH_MIPI_CLK_DIVIDER_NUM, mipiClkDiv); + write_sswitch_reg(tile, XS1_SSWITCH_MIPI_CLK_DIVIDER_NUM, mipiClkDiv); + + //write_node_config_reg(tile, XS1_SSWITCH_MIPI_CFG_CLK_DIVIDER_NUM, cfgClkDiv); + write_sswitch_reg(tile, XS1_SSWITCH_MIPI_CFG_CLK_DIVIDER_NUM, cfgClkDiv); + + /* Enable pixel demux */ + unsigned shimCfg = (((demuxMode & 0xff) << 16) | ((dataType & 0xff) << 8) | demuxEn); + //write_node_config_reg(tile, XS1_SSWITCH_MIPI_SHIM_CFG0_NUM, shimCfg); + write_sswitch_reg(tile, XS1_SSWITCH_MIPI_SHIM_CFG0_NUM, shimCfg); + + // Connect the xcore-ports to the MIPI demuxer/PHY. + int val = getps(XS1_PS_XCORE_CTRL0); + setps(XS1_PS_XCORE_CTRL0, val | 0x100); +} diff --git a/modules/mipi2/src/MipiPacketRx.xc b/modules/mipi2/src/MipiPacketRx.xc new file mode 100644 index 00000000..b934d5fd --- /dev/null +++ b/modules/mipi2/src/MipiPacketRx.xc @@ -0,0 +1,46 @@ + + +#include +#include + +#include +#include + +#include "mipi.h" + +void MipiPacketRx_init( + tileref tile, + buffered in port:32 p_mipi_rxd, + in port p_mipi_rxv, + in port p_mipi_rxa, + in port p_mipi_clk, + clock clk_mipi, + uint32_t demuxEn, + uint32_t dataType, + xMIPI_DemuxMode_t demuxMode, + uint32_t mipiClkDiv, + uint32_t cfgClkDiv) +{ + configure_in_port_strobed_slave(p_mipi_rxd, p_mipi_rxv, clk_mipi); + set_clock_src(clk_mipi, p_mipi_clk); + + /* Sample on falling edge - shim outputting on rising */ + set_clock_rise_delay(clk_mipi, 1); + set_pad_delay(p_mipi_rxa, 1); + start_clock(clk_mipi); + // take DPHY out of reset + write_node_config_reg(tile, + XS1_SSWITCH_MIPI_DPHY_CFG0_NUM, 3); + // set clock dividers to create suitable frequency + write_node_config_reg(tile, + XS1_SSWITCH_MIPI_CLK_DIVIDER_NUM, mipiClkDiv); + write_node_config_reg(tile, + XS1_SSWITCH_MIPI_CFG_CLK_DIVIDER_NUM, cfgClkDiv); + /* Enable pixel demux */ + unsigned shimCfg = (((demuxMode & 0xff) << 16) | ((dataType & 0xff) << 8) | demuxEn); + write_node_config_reg(tile, XS1_SSWITCH_MIPI_SHIM_CFG0_NUM, shimCfg); + + // Connect the xcore-ports to the MIPI demuxer/PHY. + int val = getps(XS1_PS_XCORE_CTRL0); + setps(XS1_PS_XCORE_CTRL0, val | 0x100); +} From b7d3bec27095194059d290137d32d13dcba28657 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Thu, 1 Jun 2023 16:10:51 +0100 Subject: [PATCH 027/306] renaming mipi back --- modules/CMakeLists.txt | 2 +- modules/{mipi2 => mipi}/CMakeLists.txt | 0 modules/{mipi2 => mipi}/README.md | 0 modules/{mipi2 => mipi}/api/mipi.h | 0 modules/{mipi2 => mipi}/api/mipi_defines.h | 0 modules/{mipi2 => mipi}/src/MipiPacketRx.S | 0 modules/{mipi2 => mipi}/src/MipiPacketRx.c | 0 modules/{mipi2 => mipi}/src/MipiPacketRx.xc | 0 8 files changed, 1 insertion(+), 1 deletion(-) rename modules/{mipi2 => mipi}/CMakeLists.txt (100%) rename modules/{mipi2 => mipi}/README.md (100%) rename modules/{mipi2 => mipi}/api/mipi.h (100%) rename modules/{mipi2 => mipi}/api/mipi_defines.h (100%) rename modules/{mipi2 => mipi}/src/MipiPacketRx.S (100%) rename modules/{mipi2 => mipi}/src/MipiPacketRx.c (100%) rename modules/{mipi2 => mipi}/src/MipiPacketRx.xc (100%) diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index b721e9d5..cf309c8c 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -1,6 +1,6 @@ # adding submodules here add_subdirectory(core) -add_subdirectory(mipi2) +add_subdirectory(mipi) # lib_i2c and lib_xassert do not have CMakeLists.txt # so using wrappers here include(xassert.cmake) diff --git a/modules/mipi2/CMakeLists.txt b/modules/mipi/CMakeLists.txt similarity index 100% rename from modules/mipi2/CMakeLists.txt rename to modules/mipi/CMakeLists.txt diff --git a/modules/mipi2/README.md b/modules/mipi/README.md similarity index 100% rename from modules/mipi2/README.md rename to modules/mipi/README.md diff --git a/modules/mipi2/api/mipi.h b/modules/mipi/api/mipi.h similarity index 100% rename from modules/mipi2/api/mipi.h rename to modules/mipi/api/mipi.h diff --git a/modules/mipi2/api/mipi_defines.h b/modules/mipi/api/mipi_defines.h similarity index 100% rename from modules/mipi2/api/mipi_defines.h rename to modules/mipi/api/mipi_defines.h diff --git a/modules/mipi2/src/MipiPacketRx.S b/modules/mipi/src/MipiPacketRx.S similarity index 100% rename from modules/mipi2/src/MipiPacketRx.S rename to modules/mipi/src/MipiPacketRx.S diff --git a/modules/mipi2/src/MipiPacketRx.c b/modules/mipi/src/MipiPacketRx.c similarity index 100% rename from modules/mipi2/src/MipiPacketRx.c rename to modules/mipi/src/MipiPacketRx.c diff --git a/modules/mipi2/src/MipiPacketRx.xc b/modules/mipi/src/MipiPacketRx.xc similarity index 100% rename from modules/mipi2/src/MipiPacketRx.xc rename to modules/mipi/src/MipiPacketRx.xc From e7c2214d773078886e9a599d9495e70b1b2e08f6 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Fri, 2 Jun 2023 11:41:12 +0100 Subject: [PATCH 028/306] adding readme to mipi --- modules/mipi/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/mipi/README.md b/modules/mipi/README.md index e69de29b..9a8068dc 100644 --- a/modules/mipi/README.md +++ b/modules/mipi/README.md @@ -0,0 +1,3 @@ +# MIPI interface + +This directory contains APIs for initialising MIPI slave communication and receiving data from the camera. \ No newline at end of file From 14905a2f1947f345663542985b8c503ff2f089a8 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Mon, 5 Jun 2023 09:21:33 +0100 Subject: [PATCH 029/306] * firsrt version working both examples * take picture raw still in old configuration --- camera/CMakeLists.txt | 34 +- camera/api/camera.h | 49 ++ camera/api/image_hfilter.h | 52 ++ camera/api/image_vfilter.h | 81 +++ camera/api/isp.h | 29 + camera/api/packet_handler.h | 33 + camera/api/statistics.h | 64 ++ camera/api/user_api.h | 25 + camera/api/utils.h | 29 + camera/isp.c | 233 ------- camera/isp.h | 54 -- camera/src/asm/pixel_hfilter.S | 117 ++++ camera/src/asm/pixel_vfilter_acc_init.S | 96 +++ camera/src/asm/pixel_vfilter_complete.S | 84 +++ camera/src/asm/pixel_vfilter_macc.S | 73 +++ camera/{ => src}/asm/rgb_to_yuv.S | 0 camera/{ => src}/asm/yuv_to_rgb.S | 0 camera/src/camera.xc | 75 +++ camera/src/image_hfilter.c | 133 ++++ camera/src/image_vfilter.c | 184 ++++++ camera/src/isp.c | 71 +++ camera/src/packet_handler.c | 307 ++++++++++ camera/src/statistics.c | 191 ++++++ camera/src/user_api.c | 69 +++ camera/src/utils.c | 133 ++++ camera/statistics.c | 155 ----- camera/statistics.h | 32 - camera/utils.h | 11 - examples/CMakeLists.txt | 5 +- .../CMakeLists.txt | 3 +- .../README.md | 0 examples/take_picture_downsample/src/app.c | 49 ++ examples/take_picture_downsample/src/app.h | 10 + examples/take_picture_downsample/src/main.xc | 46 ++ examples/take_picture_dynamic/src/main.xc | 22 - examples/take_picture_dynamic/src/mipi_main.h | 49 -- .../take_picture_dynamic/src/mipi_main.xc | 248 -------- .../take_picture_dynamic/src/process_frame.c | 87 --- .../take_picture_dynamic/src/process_frame.h | 20 - .../take_picture_dynamic_crop/CMakeLists.txt | 64 -- .../take_picture_dynamic_crop/src/main.xc | 22 - .../take_picture_dynamic_crop/src/mipi_main.h | 49 -- .../src/mipi_main.xc | 253 -------- .../src/process_frame.c | 96 --- .../src/process_frame.h | 20 - .../CMakeLists.txt | 0 examples/take_picture_raw/README.md | 13 + .../src/main.xc | 0 .../src/mipi_main.h | 0 .../src/mipi_main.xc | 0 .../src/process_frame.c | 0 .../src/process_frame.h | 0 modules/mipi/src/MipiPacketRx.S | 578 +++++++++--------- python/decode_downsampled.py | 42 ++ python/decode_raw8.py | 4 +- sensors/api/sensor.h | 54 +- sensors/api/sensor_control.h | 23 + sensors/imx219.h | 28 +- sensors/imx219.xc | 19 +- sensors/imx219_reg.h | 106 +--- sensors/sensor_control.xc | 28 + 61 files changed, 2524 insertions(+), 1828 deletions(-) create mode 100644 camera/api/camera.h create mode 100644 camera/api/image_hfilter.h create mode 100644 camera/api/image_vfilter.h create mode 100644 camera/api/isp.h create mode 100644 camera/api/packet_handler.h create mode 100644 camera/api/statistics.h create mode 100644 camera/api/user_api.h create mode 100644 camera/api/utils.h delete mode 100644 camera/isp.c delete mode 100644 camera/isp.h create mode 100644 camera/src/asm/pixel_hfilter.S create mode 100644 camera/src/asm/pixel_vfilter_acc_init.S create mode 100644 camera/src/asm/pixel_vfilter_complete.S create mode 100644 camera/src/asm/pixel_vfilter_macc.S rename camera/{ => src}/asm/rgb_to_yuv.S (100%) rename camera/{ => src}/asm/yuv_to_rgb.S (100%) create mode 100644 camera/src/camera.xc create mode 100644 camera/src/image_hfilter.c create mode 100644 camera/src/image_vfilter.c create mode 100644 camera/src/isp.c create mode 100644 camera/src/packet_handler.c create mode 100644 camera/src/statistics.c create mode 100644 camera/src/user_api.c create mode 100644 camera/src/utils.c delete mode 100644 camera/statistics.c delete mode 100644 camera/statistics.h delete mode 100644 camera/utils.h rename examples/{take_picture_dynamic => take_picture_downsample}/CMakeLists.txt (93%) rename examples/{take_picture => take_picture_downsample}/README.md (100%) create mode 100644 examples/take_picture_downsample/src/app.c create mode 100644 examples/take_picture_downsample/src/app.h create mode 100644 examples/take_picture_downsample/src/main.xc delete mode 100644 examples/take_picture_dynamic/src/main.xc delete mode 100644 examples/take_picture_dynamic/src/mipi_main.h delete mode 100644 examples/take_picture_dynamic/src/mipi_main.xc delete mode 100644 examples/take_picture_dynamic/src/process_frame.c delete mode 100644 examples/take_picture_dynamic/src/process_frame.h delete mode 100644 examples/take_picture_dynamic_crop/CMakeLists.txt delete mode 100644 examples/take_picture_dynamic_crop/src/main.xc delete mode 100644 examples/take_picture_dynamic_crop/src/mipi_main.h delete mode 100644 examples/take_picture_dynamic_crop/src/mipi_main.xc delete mode 100644 examples/take_picture_dynamic_crop/src/process_frame.c delete mode 100644 examples/take_picture_dynamic_crop/src/process_frame.h rename examples/{take_picture => take_picture_raw}/CMakeLists.txt (100%) create mode 100644 examples/take_picture_raw/README.md rename examples/{take_picture => take_picture_raw}/src/main.xc (100%) rename examples/{take_picture => take_picture_raw}/src/mipi_main.h (100%) rename examples/{take_picture => take_picture_raw}/src/mipi_main.xc (100%) rename examples/{take_picture => take_picture_raw}/src/process_frame.c (100%) rename examples/{take_picture => take_picture_raw}/src/process_frame.h (100%) create mode 100644 python/decode_downsampled.py create mode 100644 sensors/api/sensor_control.h create mode 100644 sensors/sensor_control.xc diff --git a/camera/CMakeLists.txt b/camera/CMakeLists.txt index 8c935d51..5cf56d09 100644 --- a/camera/CMakeLists.txt +++ b/camera/CMakeLists.txt @@ -1,13 +1,18 @@ # ############################################################################## # CMake configuration stuff +cmake_minimum_required(VERSION 3.14) +cmake_policy(SET CMP0057 NEW) enable_language(C CXX ASM) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +string(REPLACE "-MD" "-MMD" CMAKE_DEPFILE_FLAGS_C ${CMAKE_DEPFILE_FLAGS_C}) + # ############################################################################## # Target name set(LIB_NAME lib_camera) +set(LIB_NAME_ALIAS lib_camera_general) # Source files file(GLOB_RECURSE SOURCES_C "*.c") @@ -17,14 +22,25 @@ file(GLOB_RECURSE SOURCES_ASM "*.S") add_library(${LIB_NAME} STATIC) -target_include_directories(${LIB_NAME} PUBLIC . asm) +target_compile_options(${LIB_NAME} + PUBLIC + -Os + -g + -fxscope + -mcmodel=large ) + +target_include_directories(${LIB_NAME} PUBLIC . api) + +target_sources(${LIB_NAME} PRIVATE ${SOURCES_C} ${SOURCES_XC} ${SOURCES_CPP} + $<$:${SOURCES_ASM}>) + + +# ############################################################################## +# Add and link the library +add_library(${LIB_NAME_ALIAS} INTERFACE) + +target_link_libraries(${LIB_NAME} PUBLIC mipi::lib_mipi sensors::lib_imx) -target_sources(${LIB_NAME} - PRIVATE - ${SOURCES_C} - ${SOURCES_XC} - ${SOURCES_CPP} - ${SOURCES_ASM} -) +target_link_libraries(${LIB_NAME_ALIAS} INTERFACE ${LIB_NAME}) -add_library(camera::lib_camera ALIAS ${LIB_NAME}) +add_library(camera::lib_camera ALIAS ${LIB_NAME_ALIAS}) diff --git a/camera/api/camera.h b/camera/api/camera.h new file mode 100644 index 00000000..177eb19f --- /dev/null +++ b/camera/api/camera.h @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include "xs1.h" + +#include "i2c.h" +#include "mipi.h" + +#include "sensor.h" + + +#ifndef MIPI_CLKBLK +#define MIPI_CLKBLK XS1_CLKBLK_1 +#endif + +#define MIPI_TILE 1 + +#include "image_hfilter.h" +#include "image_vfilter.h" +#include "packet_handler.h" +#include "statistics.h" +#include "user_api.h" +#include "utils.h" + + +/** + * The packet buffer is where the packet decoupler will tell the MIPI receiver + * thread to store received packets. + */ +#define DEMUX_DATATYPE 0 // RESERVED +#define DEMUX_MODE 0x00 // no demux +#define DEMUX_EN 0 // DISABLE DEMUX +#define MIPI_CLK_DIV 1 // CLK DIVIDER +#define MIPI_CFG_CLK_DIV 3 // CFG DIVIDER + +#ifdef __XC__ + +void camera_main( + tileref mipi_tile, + in port p_mipi_clk, + in port p_mipi_rxa, + in port p_mipi_rxv, + buffered in port:32 p_mipi_rxd, + clock clk_mipi, + client interface i2c_master_if i2c, + streaming chanend c_user_api); + +#endif //__XC__ \ No newline at end of file diff --git a/camera/api/image_hfilter.h b/camera/api/image_hfilter.h new file mode 100644 index 00000000..ab60697a --- /dev/null +++ b/camera/api/image_hfilter.h @@ -0,0 +1,52 @@ + +#pragma once + +#include + +#include "sensor.h" + +#if defined(__XC__) || defined(__cplusplus) +extern "C" { +#endif + + +void pixel_hfilter( + int8_t output[], + const int8_t input[], + const int8_t coef[32], + const int16_t acc_hi[16], + const int16_t acc_lo[16], + const int16_t acc_shr[16], + const int32_t input_stride, + const unsigned output_count); + +void image_hfilter( + int8_t pix_out[APP_IMAGE_WIDTH_PIXELS], + const int8_t pix_in[SENSOR_RAW_IMAGE_WIDTH_PIXELS], + const unsigned channel_index); + + + +extern +const int8_t hfilter_coef_bayered_even[32]; + +extern +int8_t hfilter_red[32]; +extern +int8_t hfilter_green[32]; +extern +int8_t hfilter_blue[32]; + +extern +const int8_t hfilter_coef_bayered_odd[32]; + +extern +const int16_t hfilter_acc_init[2][16]; + +extern +const int16_t hfilter_shift[16]; + + +#if defined(__XC__) || defined(__cplusplus) +} +#endif diff --git a/camera/api/image_vfilter.h b/camera/api/image_vfilter.h new file mode 100644 index 00000000..31b04c68 --- /dev/null +++ b/camera/api/image_vfilter.h @@ -0,0 +1,81 @@ +#ifndef IMAGE_VFILTER_H +#define IMAGE_VFILTER_H + +#include + +#include "sensor.h" + + + +#define VFILTER_TAP_COUNT (5) + +#define VFILTER_DEC_FACTOR (APP_DECIMATION_FACTOR / 2) + +// Required: Low-res image width must be multiple of 16 pixels +#if (((APP_IMAGE_WIDTH_PIXELS)>>4)<<4) != (APP_IMAGE_WIDTH_PIXELS) +# error MIPI_IMAGE_LORES_WIDTH_PIX (width of decimated image) must be multiple of 16 pixels. +#endif + +// The number of accumulators required for vertical filtering. +// ceil(tap_count / dec_factor) +#define VFILTER_ACC_COUNT ((VFILTER_TAP_COUNT + VFILTER_DEC_FACTOR - 1)\ + / (VFILTER_DEC_FACTOR)) + +// The value that the next_tap gets reset to after outputting a row. +#define VFILTER_RESET_INDEX ((VFILTER_TAP_COUNT) - \ + ( (VFILTER_DEC_FACTOR) * (VFILTER_ACC_COUNT) )) + +#define VFILTER_ACC_WIDTH_SHORTS (2*APP_IMAGE_WIDTH_PIXELS) + + + +#if defined(__XC__) || defined(__cplusplus) +extern "C" { +#endif + + +typedef struct { + int next_tap; + int16_t buff[VFILTER_ACC_WIDTH_SHORTS]; +} vfilter_acc_t; + + +void pixel_vfilter_acc_init( + int16_t* accs, + const int32_t acc_value, + const unsigned pix_count); + +void pixel_vfilter_complete( + int8_t* pix_out, + const int16_t* accs, + const int16_t shifts[16], + const unsigned pix_count); + +void pixel_vfilter_macc( + int16_t* accs, + const int8_t* pix_in, + const int8_t filter[16], + const unsigned length_bytes); + + +void image_vfilter_frame_init( + vfilter_acc_t accs[]); + +unsigned image_vfilter_process_row( + int8_t output[], + vfilter_acc_t acc[], + const int8_t pixel_data[]); + +// unsigned image_vfilter_process_row_null( +// int8_t output[], +// vfilter_acc_t acc[]); + +unsigned image_vfilter_drain( + int8_t output[], + vfilter_acc_t acc[]); + +#if defined(__XC__) || defined(__cplusplus) +} +#endif + +#endif //IMAGE_VFILTER_H \ No newline at end of file diff --git a/camera/api/isp.h b/camera/api/isp.h new file mode 100644 index 00000000..a3227c27 --- /dev/null +++ b/camera/api/isp.h @@ -0,0 +1,29 @@ +#ifndef ISP_H +#define ISP_H + +#include // memset +#include // null +#include + +#include "statistics.h" + +#define DEFAULT_ALFA 1.58682 +#define DEFAULT_BETA 1.52535 + + +// ---------------------------------- AE/AGC ------------------------------ +float AE_compute_mean_skewness(global_stats_t *gstats); +uint8_t AE_is_adjusted(float sk); +uint8_t AE_compute_new_exposure(float exposure, float skewness); + +// ---------------------------------- AWB ------------------------------ +typedef struct { + float alfa; + float beta; + float gamma; +} AWB_gains_t; + +void AWB_compute_gains(global_stats_t *gstats, AWB_gains_t *gains); +void AWB_print_gains(AWB_gains_t *gains); + +#endif \ No newline at end of file diff --git a/camera/api/packet_handler.h b/camera/api/packet_handler.h new file mode 100644 index 00000000..b7daf2b7 --- /dev/null +++ b/camera/api/packet_handler.h @@ -0,0 +1,33 @@ + +#ifndef PACKET_HANDLER_H +#define PACKET_HANDLER_H + +#include + +#include "xccompat.h" + +#include "camera.h" + +#ifdef __XC__ +extern "C" { +#endif + +typedef struct +{ + mipi_header_t header; + uint8_t payload[MIPI_MAX_PKT_SIZE_BYTES]; +} mipi_packet_t; + + +void mipi_packet_handler( + streaming_chanend_t c_pkt, + streaming_chanend_t c_ctrl, + streaming_chanend_t c_out_row, + streaming_chanend_t c_user_api); + + +#ifdef __XC__ +} +#endif + +#endif // PACKET_HANDLER_H \ No newline at end of file diff --git a/camera/api/statistics.h b/camera/api/statistics.h new file mode 100644 index 00000000..552f0d3f --- /dev/null +++ b/camera/api/statistics.h @@ -0,0 +1,64 @@ + +#pragma once + +#include +#include // memset +#include // null +#include // free, alloc + +#include "xccompat.h" + +#include "sensor.h" +#include "sensor_control.h" +#include "image_hfilter.h" + +#if defined(__XC__) || defined(__cplusplus) +extern "C" { +#endif + +// Size of the channel element code space +#define CHANNEL_CARDINALITY (1<> (APP_HISTOGRAM_QUANTIZATION_BITS)) +#if (HISTOGRAM_BIN_COUNT != 64) + #error HISTOGRAM_BIN_COUNT value not currently supported. +#endif + +// Objects definitions +typedef struct { + int8_t pixels[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_WIDTH_PIXELS]; +} low_res_image_row_t; + +typedef struct { + int bins[HISTOGRAM_BIN_COUNT]; +} channel_histogram_t; + +typedef struct { + uint8_t min; + uint8_t max; + uint8_t percentile; + float skewness; + float mean; + channel_histogram_t histogram; +} channel_stats_t; + +typedef channel_stats_t global_stats_t[APP_IMAGE_CHANNEL_COUNT]; + +// Statistics compute funtions +void compute_skewness(channel_stats_t *stats); +void compute_simple_stats(channel_stats_t *stats); +void find_percentile(channel_stats_t *stats, const float fraction); + + +// Thread function +void statistics_thread( + streaming_chanend_t c_img_in, + CLIENT_INTERFACE(sensor_control_if, sc_if) + ); + +#if defined(__XC__) || defined(__cplusplus) +} +#endif + + diff --git a/camera/api/user_api.h b/camera/api/user_api.h new file mode 100644 index 00000000..596bc12a --- /dev/null +++ b/camera/api/user_api.h @@ -0,0 +1,25 @@ +#pragma once +#include "xccompat.h" +#include "sensor.h" + +#define CH APP_IMAGE_CHANNEL_COUNT +#define H APP_IMAGE_HEIGHT_PIXELS +#define W APP_IMAGE_WIDTH_PIXELS + +// Image structure +typedef struct { + int8_t pix[CH][H][W]; +} image_t; + +// User-side API +unsigned camera_capture_image( + int8_t image_buff[CH][H][W], + streaming_chanend_t c_cam_api); + +// Framework-side API +void camera_api_init(streaming_chanend_t c_api); +void camera_api_request_begin(); +void camera_api_request_complete(); +void camera_api_request_update( + const int8_t image_row[CH][W], + const unsigned row_index); diff --git a/camera/api/utils.h b/camera/api/utils.h new file mode 100644 index 00000000..b0b3543c --- /dev/null +++ b/camera/api/utils.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +#include "sensor.h" + +#define PRINT_TIME(a,b) printf("%d\n", b - a); + +#if defined(__XC__) || defined(__cplusplus) +extern "C" { +#endif + +static inline +int measure_time(){ + int y = 0; + asm volatile("gettime %0": "=r"(y)); + return y; +} + +void write_image( + const char* filename, + uint8_t image[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); +void c_memcpy(void *dst, void *src, size_t size); +void writeBMP(const char *filename, uint8_t img[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); + +#if defined(__XC__) || defined(__cplusplus) +} +#endif \ No newline at end of file diff --git a/camera/isp.c b/camera/isp.c deleted file mode 100644 index 4d1d0fae..00000000 --- a/camera/isp.c +++ /dev/null @@ -1,233 +0,0 @@ -#include "isp.h" - -// ---------------------------------- AE / AGC ------------------------------ -uint8_t csign(float x) { - return (x > 0) - (x < 0); -} - -uint8_t isp_false_position_step(float exposure, float skewness) -{ - static float a = 0; - static float fa = -1; - static float b = 80; - static float fb = 1; - static int count = 0; - float c = exposure; - float fc = skewness; - - if(csign(fc) == csign(fa)){ - a = c; fa = fc; - } - else{ - b = c; fb = fc; - } - c = b - fb*((b - a)/(fb - fa)); - - // each X samples, restart AE algorithm - if (count < 20){ - count = count + 1; - } - else{ - // restart auto exposure - count = 0; - a = 0; - fa = -1; - b = 80; - fb = 1; - } - return c; -} - - -// ---------------------------------- AWB ------------------------------ -void isp_AWB_static(const uint32_t buffsize, - uint8_t *red, - uint8_t *green, - uint8_t *blue, - float alfa, - float beta, - float delta) -{ - if (delta == 0){ // just red and blue - for (uint32_t i=0; i < buffsize; i++){ - red[i] = (uint8_t) alfa*red[i]; - blue[i] = (uint8_t) beta*blue[i]; - } - } - else{ - for (uint32_t i=0; i < buffsize; i++){ - red[i] = (uint8_t) alfa*red[i]; - blue[i] = (uint8_t) beta*blue[i]; - green[i] = (uint8_t) delta*green[i]; - } - } -} - -void isp_AWB_gray_world(const uint32_t buffsize, - uint8_t *red, - uint8_t *green, - uint8_t *blue) -{ - // compute statistics by channel - Statistics st_red; - Statistics st_green; - Statistics st_blue; - uint8_t step = 1; - - Statistics_compute_all(buffsize, step, red, &st_red); - Statistics_compute_all(buffsize, step, red, &st_green); - Statistics_compute_all(buffsize, step, red, &st_blue); - - // apply white balancing - float alfa = (st_green.mean)/(st_red.mean); - float beta = (st_green.mean)/(st_blue.mean); - isp_AWB_static(buffsize, red, green, blue, alfa, beta, 0); -} - -void isp_AWB_percentile(const uint32_t buffsize, - uint8_t *red, - uint8_t *green, - uint8_t *blue) -{ - // compute statistics by channel - Statistics st_red; - Statistics st_green; - Statistics st_blue; - uint8_t step = 1; - - Statistics_compute_all(buffsize, step, red, &st_red); - Statistics_compute_all(buffsize, step, red, &st_green); - Statistics_compute_all(buffsize, step, red, &st_blue); - - // apply white balancing - float alfa = 255/(st_red.percentile); - float beta = 255/(st_blue.percentile); - float delta = 255/(st_red.percentile); - - isp_AWB_static(buffsize, red, green, blue, alfa, beta, delta); -} - - - -// ---------------------------------- GAMMA ------------------------------ -/** -* Apply 1.8 gamma to each pixel in an image using stride 1 -* it take 255 bytes of memory, 0MUL, 0DIV, 2MEM ACCESS -* @param buffsize - Size of the buffer to operate on. -* @param img - * Pointer to the image to operate on. Modified -*/ -void isp_gamma_stride1(const uint32_t buffsize, uint8_t *img){ - // gamma naming: 1p8_s1 = gamma 1.8 , with a stride of 1 - // 1p8_s4 => img^(1/1.8) (in a normalizeed 0-1 image) - const uint8_t gamma_1p8_s1[255] = { - 0,12,17,22,25,29,32,35,37,40,42,44,47,49,51,53,55,57,58,60,62,64,65,67,69, - 70,72,73,75,76,78,79,80,82,83,85,86,87,89,90,91,92,94,95,96,97,98,100,101, - 102,103,104,105,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121, - 122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,139, - 140,141,142,143,144,145,146,146,147,148,149,150,151,152,152,153,154,155,156, - 157,157,158,159,160,161,161,162,163,164,165,165,166,167,168,169,169,170,171, - 172,172,173,174,175,175,176,177,178,178,179,180,181,181,182,183,183,184,185, - 186,186,187,188,188,189,190,191,191,192,193,193,194,195,195,196,197,198,198, - 199,200,200,201,202,202,203,204,204,205,206,206,207,208,208,209,209,210,211, - 211,212,213,213,214,215,215,216,217,217,218,218,219,220,220,221,222,222,223, - 223,224,225,225,226,226,227,228,228,229,230,230,231,231,232,233,233,234,234, - 235,236,236,237,237,238,238,239,240,240,241,241,242,243,243,244,244,245,245, - 246,247,247,248,248,249,249,250,251,251,252,252,253,253,254,255}; - - for(uint32_t i=0; i img^(1/1.8) (in a normalizeed 0-1 image) - const uint8_t gamma_1p8_s4[64] = { - 0,6,9,12,14,15,17,19,20,21,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38, - 39,39,40,41,42,42,43,44,45,45,46,47,48,48,49,50,50,51,52,52,53,54,54,55,55, - 56,57,57,58,58,59,60,60,61,61,62,62,63 - }; - static const uint8_t stride = 4; - - for(uint32_t i=0; i // memset -#include // null -#include - -#include "statistics.h" // for dynamic AWB - - -// ----------------- ISP SETTINGS ------------------------------- -#define MASK8 0xFF -#define MASK24 0x00FFFFFF -#define DEFAULT_ALFA 1.58682 // default red gain (not used for the moment) -#define DEFAULT_BETA 1.52535 // default blue gain (not used for the moment) -#define ENABLE_AE 1 // enable auto exposure -#define AE_MARGIN 0.1 // default marging for the auto exposure error -#define STEP 16 // histogram step size - -// ---------------------------------- AE/AGC ------------------------------ -uint8_t csign(float x); -uint8_t isp_false_position_step(float exposure, float skewness); - -// ---------------------------------- AWB ------------------------------ -void isp_AWB_gray_world(const uint32_t buffsize, uint8_t *red, uint8_t *green, uint8_t *blue); -void isp_AWB_percentile(const uint32_t buffsize, uint8_t *red, uint8_t *green, uint8_t *blue); -void isp_AWB_static(const uint32_t buffsize, uint8_t *red, uint8_t *green, uint8_t *blue, float alfa, float beta, float delta); - -// ---------------------------------- GAMMA ------------------------------ -void isp_gamma_stride1(const uint32_t buffsize, uint8_t *img); -void isp_gamma_stride4(const uint32_t buffsize, uint8_t *img); - -// -------------------------- ROTATE/RESIZE ------------------------------------- -void isp_bilinear_resize(const uint16_t in_width, const uint16_t in_height, - uint8_t *img, - const uint16_t out_width, const uint16_t out_height, - uint8_t *out_img); - -void isp_rotate_image(const uint8_t* src, uint8_t* dest, int width, int height); - -// -------------------------- COLOR CONVERSION ------------------------------------- -// Macro arguments -#define GET_R(x) ((int8_t)((x & MASK24)) & MASK8) -#define GET_G(x) ((int8_t)((x & MASK24) >> 8) & MASK8) -#define GET_B(x) ((int8_t)((x & MASK24) >> 16) & MASK8) -#define GET_Y(x) GET_R(x) -#define GET_U(x) GET_G(x) -#define GET_V(x) GET_B(x) - -// Converts signed yuv or rgb (-127..127, -127..127, -127..127) into signed rgb / yuv. -extern int yuv_to_rgb(int y, int u, int v); -extern int rgb_to_yuv(int r, int g, int b); - -#endif // ISP_H \ No newline at end of file diff --git a/camera/src/asm/pixel_hfilter.S b/camera/src/asm/pixel_hfilter.S new file mode 100644 index 00000000..1b7d367f --- /dev/null +++ b/camera/src/asm/pixel_hfilter.S @@ -0,0 +1,117 @@ + +#include +#include + +.issue_mode dual + +#define FUNCTION_NAME pixel_hfilter +#define NSTACKWORDS 8 + +.globl FUNCTION_NAME.nstackwords +.globl FUNCTION_NAME.maxthreads +.globl FUNCTION_NAME.maxtimers +.globl FUNCTION_NAME.maxchanends + +.linkset FUNCTION_NAME.nstackwords, NSTACKWORDS +.linkset FUNCTION_NAME.maxchanends, 0 +.linkset FUNCTION_NAME.maxtimers, 0 +.linkset FUNCTION_NAME.maxthreads, 0 + +.globl FUNCTION_NAME +.type FUNCTION_NAME, @function +.text +.cc_top FUNCTION_NAME.func, FUNCTION_NAME + +/* + **************************************************** + **************************************************** + +void pixel_hfilter( + int8_t output[], + const int8_t input[], + const int8_t coef[32], + const int16_t acc_hi[16], + const int16_t acc_lo[16], + const int16_t acc_shr[16], + const int32_t input_stride, + const unsigned output_count); + + **************************************************** + **************************************************** +*/ + +#define STK_ACC_LO (NSTACKWORDS+1) +#define STK_OUT_SHR (NSTACKWORDS+2) +#define STK_IN_STR (NSTACKWORDS+3) +#define STK_OUT_LEN (NSTACKWORDS+4) + +#define output r0 +#define input r1 +#define coef r2 +#define acc_hi r3 +// #define acc_lo r4 +#define out_shr r5 +#define in_str r6 +#define len r7 +#define _16 r8 +#define mask r9 + + +.align 4 +.skip 0 +FUNCTION_NAME: + dualentsp NSTACKWORDS + std r4, r5, sp[1] + std r6, r7, sp[2] + std r8, r9, sp[3] + + ldc r11, 0x200 +{ ldc _16, 16 ; vsetc r11 } +{ mkmsk mask, 4 ; ldw len, sp[STK_OUT_LEN] } +{ and mask, len, mask ; ldw r11, sp[STK_ACC_LO] } +// if len isn't a multiple of 16 we're gonna do a bad thing. Fix this later. +{ ecallt mask ; ldw out_shr, sp[STK_OUT_SHR]} +{ mkmsk mask, 16 ; ldw in_str, sp[STK_IN_STR] } +// Start at the end so we can proceed monotonically +// input <-- input + (len*in_str) +// output <-- output + (len) +{ add output, output, len ; vldc coef[0] } +maccu coef, input, len, in_str + +// We subtract at the beginning of the loop so it should work out correctly. + +.L_loop_top: + { sub input, input, in_str ; vldd acc_hi[0] } + { sub output, output, _16 ; vldr r11[0] } + { sub input, input, in_str ; vlmaccr input[0] } + { sub input, input, in_str ; vlmaccr input[0] } + { sub input, input, in_str ; vlmaccr input[0] } + { sub input, input, in_str ; vlmaccr input[0] } + { sub input, input, in_str ; vlmaccr input[0] } + { sub input, input, in_str ; vlmaccr input[0] } + { sub input, input, in_str ; vlmaccr input[0] } + { sub input, input, in_str ; vlmaccr input[0] } + { sub input, input, in_str ; vlmaccr input[0] } + { sub input, input, in_str ; vlmaccr input[0] } + { sub input, input, in_str ; vlmaccr input[0] } + { sub input, input, in_str ; vlmaccr input[0] } + { sub input, input, in_str ; vlmaccr input[0] } + { sub input, input, in_str ; vlmaccr input[0] } + { sub input, input, in_str ; vlmaccr input[0] } + { ; vlmaccr input[0] } + { sub len, len, _16 ; vlsat out_shr[0] } + vstrpv output[0], mask + { ; bt len, .L_loop_top } + + + + ldd r8, r9, sp[3] + ldd r6, r7, sp[2] + ldd r4, r5, sp[1] + retsp NSTACKWORDS + +.size FUNCTION_NAME, .-FUNCTION_NAME +.cc_bottom FUNCTION_NAME.func + + + diff --git a/camera/src/asm/pixel_vfilter_acc_init.S b/camera/src/asm/pixel_vfilter_acc_init.S new file mode 100644 index 00000000..30a330da --- /dev/null +++ b/camera/src/asm/pixel_vfilter_acc_init.S @@ -0,0 +1,96 @@ + +#include +#include + +.issue_mode dual + +#define FUNCTION_NAME pixel_vfilter_acc_init +#define NSTACKWORDS 12 + +.globl FUNCTION_NAME.nstackwords +.globl FUNCTION_NAME.maxthreads +.globl FUNCTION_NAME.maxtimers +.globl FUNCTION_NAME.maxchanends + +.linkset FUNCTION_NAME.nstackwords, NSTACKWORDS +.linkset FUNCTION_NAME.maxchanends, 0 +.linkset FUNCTION_NAME.maxtimers, 0 +.linkset FUNCTION_NAME.maxthreads, 0 + +.globl FUNCTION_NAME +.type FUNCTION_NAME, @function +.text +.cc_top FUNCTION_NAME.func, FUNCTION_NAME + +/* + **************************************************** + **************************************************** + + Initialize a row of accumulators for vertical decimation. + + void pixel_vfilter_acc_init( + int16_t* accs, + const int32_t acc_value, + const unsigned pix_count); + + **************************************************** + **************************************************** +*/ + +#define STK_VEC_TMP (NSTACKWORDS-8) + +#define accs r0 +#define value r1 +#define len r2 +#define tmp r3 + +#define _32 r4 + + +.align 4 +.skip 0 +FUNCTION_NAME: + dualentsp NSTACKWORDS + std r4, r5, sp[1] + +// broadcast the high and low accumulator initialization values into vectors + +{ ; mov tmp, value } + zip tmp, value, 4 +{ ldaw r11, sp[STK_VEC_TMP] ; ldc _32, 32 } + std tmp, tmp, r11[0] + std tmp, tmp, r11[1] + std tmp, tmp, r11[2] + std tmp, tmp, r11[3] +{ shr r5, len, 4 ; vldr r11[0] } + std value, value, r11[0] + std value, value, r11[1] + std value, value, r11[2] + std value, value, r11[3] +{ zext len, 4 ; vldd r11[0] } + +// for now, assume the len is multiple of 16 and raise an exception if not. +// Part of the problem with supporting a tail is that if it is supported here +// it must also be supported everywhere else. Because the high and low half +// words are split into groups of 16 (required by VPU), if the last group has +// for example, 4, then how do we place those in memory? like: +// acc_hi,acc_hi,acc_hi,acc_hi,acc_lo,acc_lo,acc_lo,acc_lo ? +// or like: +// acc_hi,acc_hi,acc_hi,acc_hi,0,0,0,0,0,0,0,0,0,0,0,0,acc_lo,acc_lo,acc_lo,acc_lo,0,0,0,0,0,0,0,0,0,0,0,0,? +// if the former, then all the other functions need to know that spacing. If +// the latter, then we're effectively already requiring that space be +// allocated for a multiple of 16 anyway. + ecallt len + +.L_loop_top: + { add accs, accs, _32 ; vstd accs[0] } + { sub r5, r5, 1 ; vstr accs[0] } + { add accs, accs, _32 ; bt r5, .L_loop_top } + + +.L_finish: + ldd r4, r5, sp[1] + retsp NSTACKWORDS + +.size FUNCTION_NAME, .-FUNCTION_NAME +.cc_bottom FUNCTION_NAME.func diff --git a/camera/src/asm/pixel_vfilter_complete.S b/camera/src/asm/pixel_vfilter_complete.S new file mode 100644 index 00000000..0939537a --- /dev/null +++ b/camera/src/asm/pixel_vfilter_complete.S @@ -0,0 +1,84 @@ + +#include +#include + +.issue_mode dual + +#define FUNCTION_NAME pixel_vfilter_complete +#define NSTACKWORDS 6 + +.globl FUNCTION_NAME.nstackwords +.globl FUNCTION_NAME.maxthreads +.globl FUNCTION_NAME.maxtimers +.globl FUNCTION_NAME.maxchanends + +.linkset FUNCTION_NAME.nstackwords, NSTACKWORDS +.linkset FUNCTION_NAME.maxchanends, 0 +.linkset FUNCTION_NAME.maxtimers, 0 +.linkset FUNCTION_NAME.maxthreads, 0 + +.globl FUNCTION_NAME +.type FUNCTION_NAME, @function +.text +.cc_top FUNCTION_NAME.func, FUNCTION_NAME + +/* + **************************************************** + **************************************************** + + Call after accumulation to output a line of pixels. + + pix_count must be a multiple of 4. + + void pixel_vfilter_complete( + int8_t* pix_out, + const int16_t* accs, + const int16_t shifts[16], + const unsigned pix_count); + + **************************************************** + **************************************************** +*/ + +#define pix_out r0 +#define accs r1 +#define shifts r2 +#define len r3 + +#define _32 r4 +#define _16 r5 + +#define mask r6 +#define tmp r7 + + +.align 4 +.skip 0 +FUNCTION_NAME: + dualentsp NSTACKWORDS + std r4, r5, sp[1] + std r6, r7, sp[2] + + ldc r11, 0x200 +{ ldc _32, 32 ; vsetc r11 } +{ ldc _16, 16 ; mov tmp, len } +{ shr len, len, 4 ; zext tmp, 4 } +{ mkmsk mask, _16 ; bu .L_loop } + +// len must be a multiple of 16 + ecallt tmp + +.align 16 +.L_loop: + { add r11, accs, _32 ; vldd accs[0] } + { add accs, r11, _32 ; vldr r11[0] } + { sub len, len, 1 ; vlsat shifts[0] } + vstrpv pix_out[0], mask + { add pix_out, pix_out, _16 ; bt len, .L_loop } + + ldd r6, r7, sp[2] + ldd r4, r5, sp[1] + retsp NSTACKWORDS + +.size FUNCTION_NAME, .-FUNCTION_NAME +.cc_bottom FUNCTION_NAME.func diff --git a/camera/src/asm/pixel_vfilter_macc.S b/camera/src/asm/pixel_vfilter_macc.S new file mode 100644 index 00000000..d3c9d4cd --- /dev/null +++ b/camera/src/asm/pixel_vfilter_macc.S @@ -0,0 +1,73 @@ + +#include +#include + +.issue_mode dual + +#define FUNCTION_NAME pixel_vfilter_macc +#define NSTACKWORDS 4 + +.globl FUNCTION_NAME.nstackwords +.globl FUNCTION_NAME.maxthreads +.globl FUNCTION_NAME.maxtimers +.globl FUNCTION_NAME.maxchanends + +.linkset FUNCTION_NAME.nstackwords, NSTACKWORDS +.linkset FUNCTION_NAME.maxchanends, 0 +.linkset FUNCTION_NAME.maxtimers, 0 +.linkset FUNCTION_NAME.maxthreads, 0 + +.globl FUNCTION_NAME +.type FUNCTION_NAME, @function +.text +.cc_top FUNCTION_NAME.func, FUNCTION_NAME + +/* + **************************************************** + **************************************************** + + void pixel_vfilter_macc( + int16_t* accs, + const int8_t* pix_in, + const int8_t filter[16], + const unsigned length_bytes); + + **************************************************** + **************************************************** +*/ + +#define accs r0 +#define pix_in r1 +#define filter r2 +#define len r3 + +#define _32 r4 + + +.align 4 +.skip 0 +FUNCTION_NAME: + dualentsp NSTACKWORDS + std r4, r5, sp[1] + + ldc r11, 0x200 +{ mov r4, len ; shr len, len, 4 } +{ zext r4, 4 ; vsetc r11 } +//len must be multiple of 16 +{ ecallt r4 ; vldc filter[0] } +{ ldc _32, 32 ; bu .L_acc_loop } + +.align 16 +.L_acc_loop: + { add r11, accs, _32 ; vldd accs[0] } + { sub len, len, 1 ; vldr r11[0] } + { add pix_in, pix_in, 8 ; vlmacc pix_in[0] } + { add accs, r11, _32 ; vstd accs[0] } + { add pix_in, pix_in, 8 ; vstr r11[0] } + { ; bt len, .L_acc_loop } + + ldd r4, r5, sp[1] + retsp NSTACKWORDS + +.size FUNCTION_NAME, .-FUNCTION_NAME +.cc_bottom FUNCTION_NAME.func diff --git a/camera/asm/rgb_to_yuv.S b/camera/src/asm/rgb_to_yuv.S similarity index 100% rename from camera/asm/rgb_to_yuv.S rename to camera/src/asm/rgb_to_yuv.S diff --git a/camera/asm/yuv_to_rgb.S b/camera/src/asm/yuv_to_rgb.S similarity index 100% rename from camera/asm/yuv_to_rgb.S rename to camera/src/asm/yuv_to_rgb.S diff --git a/camera/src/camera.xc b/camera/src/camera.xc new file mode 100644 index 00000000..232b3328 --- /dev/null +++ b/camera/src/camera.xc @@ -0,0 +1,75 @@ + +#include +#include +#include +#include + +#include +#include // for ports +#include + +#include "i2c.h" +#include "camera.h" +#include "mipi_defines.h" +#include "packet_handler.h" +#include "statistics.h" +#include "sensor_control.h" + + + +void camera_main( + tileref mipi_tile, + in port p_mipi_clk, + in port p_mipi_rxa, + in port p_mipi_rxv, + buffered in port:32 p_mipi_rxd, + clock clk_mipi, + client interface i2c_master_if i2c, + streaming chanend c_user_api) +{ + + streaming chan c_pkt; + streaming chan c_ctrl; + streaming chan c_stat_thread; + + sensor_control_if sc_if; + // chan c_sensor_control; + + // See AN for MIPI shim + // 0x7E42 >> 0111 1110 0100 0010 + // in the explorer BOARD DPDN is swap + write_node_config_reg(mipi_tile, + XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, + 0x7E42); //TODO decompose into different values + + // send packet to MIPI shim + MipiPacketRx_init(mipi_tile, + p_mipi_rxd, + p_mipi_rxv, + p_mipi_rxa, + p_mipi_clk, + clk_mipi, + DEMUX_EN, + DEMUX_DATATYPE, + DEMUX_MODE, + MIPI_CLK_DIV, + MIPI_CFG_CLK_DIV); + + // Start camera and its configurations + int r = 0; + r |= camera_init(i2c); + delay_milliseconds(100); //TODO include this inside the function + r |= camera_configure(i2c); + delay_milliseconds(600); + r |= camera_start(i2c); + delay_milliseconds(2000); + + // start the different jobs (packet controller, handler, and post_process) + par + { + MipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); + mipi_packet_handler(c_pkt, c_ctrl, c_stat_thread, c_user_api); + statistics_thread(c_stat_thread, sc_if); + sensor_control(sc_if, i2c); + } +} diff --git a/camera/src/image_hfilter.c b/camera/src/image_hfilter.c new file mode 100644 index 00000000..8430015e --- /dev/null +++ b/camera/src/image_hfilter.c @@ -0,0 +1,133 @@ + +#include +#include +#include + +#include "image_hfilter.h" + + +// The filter coefficients for the horizontal filter. Because the VPU (and in +// particular, VLMACCR) is used, this needs to be 32 bytes long, padded with +// zeros where necessary. + +// VPU loads must be 4-byte-aligned. So when filtering e.g. a bayered RGGB +// image we can't filter red, move up one byte in the pixel row, and apply the +// same filter again. Instead we need a pair of filters, containing the same +// coefficients, but offset by one index. Then, when horizontally filtering the +// G channel we provide the filter function the SAME (word-aligned) start +// address that we provided with the R channel, but using the offset +// coefficients so that the R channel is ignored instead of the G channel. +// Additionally, note that to avoid mixing channels, every other coefficient +// must be zero, in each case. +#define HFILTER_INPUT_STRIDE (APP_DECIMATION_FACTOR) + +// float hfilter_coef[3] = { 0.20872991, 0.58254019, 0.20872991 }; + +#define A (0x1B) +#define B (0x4B) +#define C (0x64) + +const int8_t hfilter_coef_bayered_even[32] = { + 0x1B,0x00,0x4B,0x00,0x1B,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +}; + +const int8_t hfilter_coef_bayered_odd[32] = { + 0x00,0x1B,0x00,0x4B,0x00,0x1B,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +}; + + +//#define RED_GAIN 1 +//#define GREEN_GAIN 1 +//#define BLUE_GAIN 1 + +#define RED_GAIN 1//1.3 +#define GREEN_GAIN 0.8//1 +#define BLUE_GAIN 1//1.3 + +int8_t hfilter_red[32] = { + 0x1B,0x00,0x4B,0x00,0x1B,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +}; +int8_t hfilter_green[32] = { + 0x1B,0x00,0x4B,0x00,0x1B,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +}; +int8_t hfilter_blue[32] = { + 0x1B,0x00,0x4B,0x00,0x1B,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +}; + + +/// ---------------------------------------------------------------- +const int16_t hfilter_acc_init[2][16] = { + { 0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000, + 0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000, }, + { 0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000, + 0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000, }, +}; +const int16_t hfilter_shift[16] = {7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7}; + + +int8_t compute_gain(int8_t num, float f) { + float result = f * (num + 127.0f); + if (result >= 255.0f) { + return 127; + } else if (result <= 0.0f) { + return -127; + } + result -= 127.0f; + return result; +} + + +void image_hfilter( + int8_t pix_out[APP_IMAGE_WIDTH_PIXELS], + const int8_t pix_in[SENSOR_RAW_IMAGE_WIDTH_PIXELS], + const unsigned channel_index) +{ + // modify filters + hfilter_red[0] = compute_gain(A, RED_GAIN); + hfilter_red[2] = compute_gain(B, RED_GAIN); + hfilter_red[4] = compute_gain(A, RED_GAIN); + + hfilter_green[1] = compute_gain(A, GREEN_GAIN); + hfilter_green[3] = compute_gain(B, GREEN_GAIN); + hfilter_green[5] = compute_gain(A, GREEN_GAIN); + + hfilter_blue[1] = compute_gain(A, BLUE_GAIN); + hfilter_blue[3] = compute_gain(B, BLUE_GAIN); + hfilter_blue[5] = compute_gain(A, BLUE_GAIN); + + // group filters + int8_t* channel_hfilter_coefs_rggb[3] = { + &hfilter_red[0], + &hfilter_green[0], + &hfilter_blue[0], + }; + + assert(channel_index >= 0 && channel_index <= 2); + + pixel_hfilter(&pix_out[0], + &pix_in[0], + channel_hfilter_coefs_rggb[channel_index], + &hfilter_acc_init[0][0], + &hfilter_acc_init[1][0], + hfilter_shift, + HFILTER_INPUT_STRIDE, + APP_IMAGE_WIDTH_PIXELS); +} + + + diff --git a/camera/src/image_vfilter.c b/camera/src/image_vfilter.c new file mode 100644 index 00000000..ab4e61e5 --- /dev/null +++ b/camera/src/image_vfilter.c @@ -0,0 +1,184 @@ + +#include +#include +#include + +#include "image_vfilter.h" + +#define USE_SIMPLE_FILTER 0 + +// float vfilter_coef[5] = { +// 0.0248892, 0.2528858, 0.44445, 0.2528858, 0.0248892 +// }; + + +#if USE_SIMPLE_FILTER + + static + const int32_t vfilter_acc_offset = 0; + + static + const int8_t vfilter_coef[5][16] = { + {0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1}, + {0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0}, + {0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0}, + {0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0}, + {0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0}, + }; + + static + const int16_t vfilter_shift[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,}; + +#else + + static + const int32_t vfilter_acc_offset = 0; + + static + const int8_t vfilter_coef[5][16] = { + { 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,}, + { 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65,}, + {114,114,114,114,114,114,114,114,114,114,114,114,114,114,114,114,}, + { 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65,}, + { 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,}, + }; + + static + const int16_t vfilter_shift[16] = {8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,}; + +#endif + + + +/** + * Prepare the provided accumulator struct for accumulation. + */ +static inline +void image_vfilter_reset( + vfilter_acc_t* acc) +{ + acc->next_tap = VFILTER_RESET_INDEX; + pixel_vfilter_acc_init(acc->buff, vfilter_acc_offset, + APP_IMAGE_WIDTH_PIXELS); +} + + +/** + * Call this once at the start of each frame. The accumulator next_tap values + * are set somewhat differently than image_vfilter_reset(), because the behavior + * at the start of the image is a little different. + * + * `acc_count` is the number of ROWS of accumulators, whereas + * `image_width_lores` is the number of pixels per low-resolution image row + * (which is the number of individual accumulators PER ROW). + * + * `image_width_lores` must be a multiple of 16 (atm) + * + * NOTE: THE VFILTER FUNCTIONS ARE CHANNEL AGNOSTIC, SO IF THE IMAGE IS + * SEPARATED INTO DIFFERENT COLOR PLANES, THIS WILL NEED TO BE CALLED ONCE + * FOR EACH COLOR PLANE. IF THEY'RE INTERLEAVED, IT CAN BE CALLED ONCE, + * BUT THE IMAGE WIDTH MUST THEN BE THE WIDTH IN _BYTES_. AND IT MUST + * STILL BE A MULTIPLE OF 16. + */ +void image_vfilter_frame_init( + vfilter_acc_t accs[]) +{ + for(int k = 0; k < VFILTER_ACC_COUNT; k++){ + accs[k].next_tap = -(VFILTER_DEC_FACTOR) * k + (VFILTER_TAP_COUNT/2); + pixel_vfilter_acc_init(accs[k].buff, vfilter_acc_offset, + APP_IMAGE_WIDTH_PIXELS); + } +} + +/** + * + * + */ +unsigned image_vfilter_process_row( + int8_t output[], + vfilter_acc_t acc[], + const int8_t pixel_data[]) +{ + for(int k = 0; k < VFILTER_ACC_COUNT; k++){ + if(acc[k].next_tap >= 0){ + pixel_vfilter_macc(acc[k].buff, + pixel_data, + &vfilter_coef[acc[k].next_tap][0], + APP_IMAGE_WIDTH_PIXELS); + } + acc[k].next_tap++; + } + + for(int k = 0; k < VFILTER_ACC_COUNT; k++){ + if(acc[k].next_tap != VFILTER_TAP_COUNT) continue; + + // produce an output row from accumulator + pixel_vfilter_complete(output, + acc[k].buff, + vfilter_shift, + APP_IMAGE_WIDTH_PIXELS); + + // reset the accumulator + image_vfilter_reset(&acc[k]); + + return 1; + } + + return 0; +} + +/** + * + * + */ +unsigned image_vfilter_process_row_null( + int8_t output[], + vfilter_acc_t acc[]) +{ + for(int k = 0; k < VFILTER_ACC_COUNT; k++){ + acc[k].next_tap++; + } + + for(int k = 0; k < VFILTER_ACC_COUNT; k++){ + if(acc[k].next_tap != VFILTER_TAP_COUNT) continue; + + // produce an output row from accumulator + pixel_vfilter_complete(output, + acc[k].buff, + vfilter_shift, + APP_IMAGE_WIDTH_PIXELS); + + // reset the accumulator + image_vfilter_reset(&acc[k]); + + return 1; + } + + return 0; +} + +/** + * After the last line of the image, some of the accumulators will be midway + * through processing the image but still need to be output without maccing + * any more inputs. + * + * Keep calling this until it returns 0. + */ +unsigned image_vfilter_drain( + int8_t output[], + vfilter_acc_t acc[]) +{ + for(int k = 0; k < VFILTER_ACC_COUNT; k++){ + if(acc[k].next_tap <= 0) continue; + + pixel_vfilter_complete(output, + acc[k].buff, + vfilter_shift, + APP_IMAGE_WIDTH_PIXELS); + acc[k].next_tap = 0; + + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/camera/src/isp.c b/camera/src/isp.c new file mode 100644 index 00000000..6afdbf44 --- /dev/null +++ b/camera/src/isp.c @@ -0,0 +1,71 @@ +#include "isp.h" + +#define AE_MARGIN 0.1 // defaukt marging for the auto exposure error + + +// ---------------------------------- utils ------------------------------ +static +uint8_t csign(float x) { + return (x > 0) - (x < 0); +} + +// ---------------------------------- AE / AGC ------------------------------ +float AE_compute_mean_skewness(global_stats_t *gstats){ + float sk = 0.0; + sk += (*gstats)[0].skewness; + sk += (*gstats)[1].skewness; + sk += (*gstats)[2].skewness; + sk = sk / 3; + return sk; +} + +uint8_t AE_is_adjusted(float sk) { + return (sk < AE_MARGIN && sk > -AE_MARGIN) ? 1 : 0; +} + +uint8_t AE_compute_new_exposure(float exposure, float skewness) +{ + static float a = 0; // minimum value for exposure + static float fa = -1; // minimimum skewness + static float b = 80; // maximum value for exposure + static float fb = 1; // minimum skewness + static int count = 0; + float c = exposure; + float fc = skewness; + + if(csign(fc) == csign(fa)){ + a = c; fa = fc; + } + else{ + b = c; fb = fc; + } + c = b - fb*((b - a)/(fb - fa)); + + // each X samples, restart AE algorithm + if (count < 20){ + count = count + 1; + } + else{ + // restart auto exposure + count = 0; + a = 0; + fa = -1; + b = 80; + fb = 1; + } + return (uint8_t)c; +} + + +// ---------------------------------- AWB ------------------------------ +void AWB_compute_gains(global_stats_t *gstats, AWB_gains_t *gains){ + // Adjust AWB + const float ceil = 254.0; + gains->alfa = ceil / (float)(*gstats)[0].percentile; // RED + gains->beta = ceil / (float)(*gstats)[1].percentile; // GREEN + gains->gamma = ceil / (float)(*gstats)[2].percentile; // BLUE +} + +void AWB_print_gains(AWB_gains_t *gains){ + printf("awb:%f,%f,%f\n",gains->alfa,gains->beta,gains->gamma); +} \ No newline at end of file diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c new file mode 100644 index 00000000..dcf29f20 --- /dev/null +++ b/camera/src/packet_handler.c @@ -0,0 +1,307 @@ + + +#include +#include + +#include + +#include "packet_handler.h" +#include "image_vfilter.h" +#include "image_hfilter.h" +#include "user_api.h" +#include "utils.h" +#include "sensor.h" + +/** + * State needed for the vertical filter + */ +static +vfilter_acc_t vfilter_accs[APP_IMAGE_CHANNEL_COUNT][VFILTER_ACC_COUNT]; + + +/** + * Contains the local state info for the packet handler thread. + */ +static struct { + unsigned wait_for_frame_start; + unsigned frame_number; + unsigned in_line_number; + unsigned out_line_number; +} ph_state = { + .wait_for_frame_start = 1, + .frame_number = 0, + .in_line_number = 0, + .out_line_number = 0, +}; + + +static +void handle_frame_start( + const mipi_packet_t* pkt) +{ + // New frame is starting, reset the vertical filter accumulator states. + for(int c = 0; c < APP_IMAGE_CHANNEL_COUNT; c++){ + image_vfilter_frame_init(&vfilter_accs[c][0]); + } + + camera_api_request_begin(); +} + + + +static +void handle_unknown_packet( + const mipi_packet_t* pkt) +{ + // Do nothing +} + +#define BYPASS_HFILTER 0 +/** + * Handle a row of pixel data. + * + * This function handles horizontal and vertical decimation of the image data. + * + * Returns true iff output_buffer[][] has been filled and is ready to be sent to + * the next thread. + */ +static +unsigned handle_pixel_data( + const mipi_packet_t* pkt, + int8_t output_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_WIDTH_PIXELS]) +{ + + // Bayer pattern is RGGB; even index rows have RG data, + // odd index rows have GB data. + unsigned pattern = ph_state.in_line_number % 2; + + // Temporary buffer to store horizontally-filtered row data. [1] + int8_t hfilt_row[APP_IMAGE_WIDTH_PIXELS]; + + if(pattern == 0){ // Packet contains RGRGRGRGRGRGRGRGRG... + ////// RED + image_hfilter(&hfilt_row[0], + (int8_t*) &pkt->payload[0], + CHAN_RED); + + image_vfilter_process_row(&output_buffer[CHAN_RED][0], + &vfilter_accs[CHAN_RED][0], + &hfilt_row[0]); + + ////// GREEN + image_hfilter(&hfilt_row[0], + (int8_t*) &pkt->payload[0], + CHAN_GREEN); + + // we now it is not the las row [2] + image_vfilter_process_row(&output_buffer[CHAN_GREEN][0], + &vfilter_accs[CHAN_GREEN][0], + &hfilt_row[0]); + + } + else { // Packet contains GBGBGBGBGBGBGBGBGBGB... + ////// BLUE + image_hfilter(&hfilt_row[0], + (int8_t*) &pkt->payload[0], + CHAN_BLUE); + + unsigned new_row = image_vfilter_process_row( + &output_buffer[CHAN_BLUE][0], + &vfilter_accs[CHAN_BLUE][0], + &hfilt_row[0]); + + // If new_row is true, then the vertical decimator has output a new row for + // each of the three color channels, and so we should signal this upwards. + if(new_row){ + return 1; + } + } + return 0; +} + + +static +void on_new_output_row( + const int8_t pix_out[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_WIDTH_PIXELS], + streaming_chanend_t c_out_row) +{ + // Pass the output row along for statistics processing + s_chan_out_word(c_out_row, (unsigned) &pix_out[0][0] ); + + // Write image data to user buffer if there is one waiting + camera_api_request_update(pix_out, ph_state.out_line_number); + + ph_state.out_line_number++; +} + + + +static +void handle_frame_end( + int8_t pix_out[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_WIDTH_PIXELS], + const mipi_packet_t* pkt, + streaming_chanend_t c_out_row) +{ + // Drain the vertical filter's accumulators + image_vfilter_drain(&pix_out[CHAN_RED][0], &vfilter_accs[CHAN_RED][0]); + image_vfilter_drain(&pix_out[CHAN_GREEN][0], &vfilter_accs[CHAN_GREEN][0]); + if(image_vfilter_drain(&pix_out[CHAN_BLUE][0], &vfilter_accs[CHAN_BLUE][0])){ + // Pass final row(s) to the statistics thread + on_new_output_row(pix_out, c_out_row); + } + + // Signal statistics thread to do frame-end work by sending NULL. + s_chan_out_word(c_out_row, (unsigned) NULL); + + // If user is waiting for image, this signals that it's done. + camera_api_request_complete(); + + // printf("\n"); + // printf("out lines: %u\n", ph_state.out_line_number); + // printf("in lines: %u\n", ph_state.in_line_number); +} + + +void handle_no_expected_lines(){ + if(ph_state.in_line_number >= SENSOR_RAW_IMAGE_HEIGHT_PIXELS){ + // We've received more lines of image data than we expected. + #ifdef ASSERT_ON_TOO_MANY_LINES + assert(0); + #endif + } +} + +/** + * Process a single packet. + * + * This function keeps track of where we are within the input and output image + * frames. It also passes the packet along to a function for processing + * depending upon the data type. + */ +static +void handle_packet( + const mipi_packet_t* pkt, + streaming_chanend_t c_out_row) +{ + + /* + * These buffers store rows of the fully decimated image. They are passed + * along to the statistics thread once the packet handler thread no longer + * needs them. + * + * Two are needed -- the one the decimator is currently filling, and the one + * that the statistics thread is currently using. + */ + __attribute__((aligned(8))) + static int8_t output_buff[2][APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_WIDTH_PIXELS]; + static unsigned out_dex = 0; + + + // definitions + const mipi_header_t header = pkt->header; + const mipi_data_type_t data_type = MIPI_GET_DATA_TYPE(header); + + // At start-up we usually want to wait for a new frame before processing + // anything + if(ph_state.wait_for_frame_start + && data_type != MIPI_DT_FRAME_START) return; + + /* + The idea here is that logic that keeps the packet handler in a coherent + state, like tracking frame and line numbers, should go directly in here, but + logic that actually interprets, processes or reacts to packet data should go + into the individual functions. + */ + switch(data_type) + { + case MIPI_DT_FRAME_START: + ph_state.wait_for_frame_start = 0; + ph_state.in_line_number = 0; + ph_state.out_line_number = 0; + ph_state.frame_number++; + + handle_frame_start(pkt); + break; + + case MIPI_DT_FRAME_END: + handle_frame_end(output_buff[out_dex], pkt, c_out_row); + out_dex = 1 - out_dex; + break; + + case MIPI_EXPECTED_FORMAT: + handle_no_expected_lines(); + + if(handle_pixel_data(pkt, output_buff[out_dex])){ + on_new_output_row(output_buff[out_dex], c_out_row); + out_dex = 1 - out_dex; + } + + ph_state.in_line_number++; + break; + + default: + // We've received a packet we don't know how to interpret. + handle_unknown_packet(pkt); + break; + } +} + + +/** + * Top level of the packet handling thread. Receives MIPI packets from the + * packet receiver and passes them to `handle_packet()` for parsing and + * processing. + */ +void mipi_packet_handler( + streaming_chanend_t c_pkt, + streaming_chanend_t c_ctrl, + streaming_chanend_t c_out_row, + streaming_chanend_t c_user_api) +{ + /* + * These buffers will be used to hold received MIPI packets while they're + * being processed. + */ + __attribute__((aligned(8))) + mipi_packet_t packet_buffer[MIPI_PKT_BUFFER_COUNT]; + unsigned pkt_idx = 0; + + camera_api_init(c_user_api); + + // Give the MIPI packet receiver a first buffer + s_chan_out_word(c_pkt, (unsigned) &packet_buffer[pkt_idx] ); + + while(1) { + pkt_idx = (pkt_idx + 1) & (MIPI_PKT_BUFFER_COUNT-1); + + mipi_packet_t * pkt = (mipi_packet_t*) s_chan_in_word(c_pkt); + // Swap buffers with the receiver thread. Give it the next buffer + // to fill and take the last filled buffer from it. + s_chan_out_word(c_pkt, (unsigned) &packet_buffer[pkt_idx] ); + + // Process the packet + //const mipi_header_t header = pkt->header; + //const mipi_data_type_t data_type = MIPI_GET_DATA_TYPE(header); + + // unsigned time_start = measure_time(); + handle_packet(pkt, c_out_row); + // unsigned time_proc = measure_time() - time_start; + + } +} + + + +// NOTES +//[1] +/* + // NOTE: If vertical filtering has to go into a separate thread, this buffer + // will need to be passed to another thread and must be done differently. + // NOTE: This buffer will only hold one color channel at a time because the + // horizontal filter separates color planes. + +[2] +// We don't need to watch for new rows from the vertical decimator here +// because we know a priori that blue will be the last one out. + +*/ \ No newline at end of file diff --git a/camera/src/statistics.c b/camera/src/statistics.c new file mode 100644 index 00000000..ccf98e3e --- /dev/null +++ b/camera/src/statistics.c @@ -0,0 +1,191 @@ + + +#include +#include "xccompat.h" + +#include "statistics.h" +#include "isp.h" + +// Number of samples taken each row +#define HISTOGRAM_SAMPLE_PER_ROW ((APP_IMAGE_WIDTH_PIXELS + APP_HISTOGRAM_SAMPLE_STEP - 1) / (APP_HISTOGRAM_SAMPLE_STEP)) +// Number of samples taken in an image +#define HISTOGRAM_TOTAL_SAMPLES (HISTOGRAM_SAMPLE_PER_ROW * APP_IMAGE_HEIGHT_PIXELS ) +// This is the normalization factor +static const float histogram_norm_factor = (1.0 / (float) HISTOGRAM_TOTAL_SAMPLES); +// Initial exposure +uint8_t new_exp = 35; + + + +/** + * //TODO + */ +void update_histogram( + channel_histogram_t* hist, + const int8_t pix[]) +{ + for(int k = 0; k < APP_IMAGE_WIDTH_PIXELS; k += APP_HISTOGRAM_SAMPLE_STEP){ + int val = pix[k]; + val += 128; // convert from int8_t to uint8_t + val >>= APP_HISTOGRAM_QUANTIZATION_BITS; + hist->bins[val]++; + } +} + + +/** + * //TODO + */ +void compute_skewness(channel_stats_t *stats) +{ + const float zk_values[] = { + -1.000000, -0.907753, -0.821362, -0.740633, -0.665375, -0.595396, + -0.530504, -0.470508, -0.415214, -0.364431, -0.317968, -0.275632, + -0.237231, -0.202574, -0.171468, -0.143721, -0.119142, -0.097538, + -0.078717, -0.062488, -0.048659, -0.037037, -0.027431, -0.019648, + -0.013497, -0.008786, -0.005323, -0.002915, -0.001372, -0.000005, + -0.000108, -4e-06, 4e-06, 0.000108, 0.0005, 0.001372, + 0.002915, 0.005323, 0.008786, 0.013497, 0.019648, 0.027431, + 0.037037, 0.048659, 0.062488, 0.078717, 0.097538, 0.119142, + 0.143721, 0.171468, 0.202574, 0.237231, 0.275632, 0.317968, + 0.364431, 0.415214, 0.470508, 0.530504, 0.595396, 0.665375, + 0.740633, 0.821362, 0.907753, 1.0}; + + float skew = 0.0; + for(int k = 0; k < HISTOGRAM_BIN_COUNT; k++){ + skew += zk_values[k] * stats->histogram.bins[k]; + } + + // Normnalization [3] + stats -> skewness = skew * histogram_norm_factor; +} + + +/** + * //TODO + */ +void compute_simple_stats(channel_stats_t *stats) +{ + // Calculate the histogram + for(int k = 0; k < HISTOGRAM_BIN_COUNT; k++){ + unsigned bin = stats->histogram.bins[k]; + stats->mean += bin * k; + stats->max = (stats->max >= bin)? stats->max : bin; + stats->min = (stats->min <= bin)? stats->min : bin; + } + + // biased downwards due to truncation + stats->max <<= APP_HISTOGRAM_QUANTIZATION_BITS; + stats->min <<= APP_HISTOGRAM_QUANTIZATION_BITS; + stats->mean *= (1<histogram.bins[k]; + if(total < threshold && new_total >= threshold) + result = (k << APP_HISTOGRAM_QUANTIZATION_BITS); + total = new_total; + } + stats -> percentile = (uint8_t) result; +} + +//////////////////////////// + + +void statistics_thread( + streaming_chanend_t c_img_in, + CLIENT_INTERFACE(sensor_control_if, sc_if)) +{ + // Outer loop iterates over frames + while(1){ + // Declare new stats + global_stats_t global_stats = {{0}}; + AWB_gains_t awb_gains = {0}; + // Inner loop iterates over rows within a frame + while(1){ + + low_res_image_row_t* row = (low_res_image_row_t*) s_chan_in_word(c_img_in); + + // Signal end of frame [1] + if(row == NULL) + break; + + for(uint8_t channel = 0; channel < APP_IMAGE_CHANNEL_COUNT; channel++){ + update_histogram( + &global_stats[channel].histogram, + &row->pixels[channel][0] + ); + } + } + // End of frame + for(uint8_t channel = 0; channel < APP_IMAGE_CHANNEL_COUNT; channel++){ + compute_skewness(&global_stats[channel]); + compute_simple_stats(&global_stats[channel]); + find_percentile(&global_stats[channel], APP_WB_PERCENTILE); + } + + // TODO delete this os leave it as a function + printf("skewness:%f,%f,%f\n", + global_stats[0].skewness, + global_stats[1].skewness, + global_stats[2].skewness); + + float sk = AE_compute_mean_skewness(&global_stats); + if (AE_is_adjusted(sk)){ + printf("-----> adjustement done\n"); + } + else{ + // adjust exposure + new_exp = AE_compute_new_exposure((float) new_exp, sk); + sensor_control_set_exposure(sc_if, (uint8_t) new_exp); + } + + // Adjust AWB + AWB_compute_gains(&global_stats, &awb_gains); + AWB_print_gains(&awb_gains); + + // Adjust AWB + //float alfa = 255.0/global_stats[0].percentile; // RED + //float beta = 255.0/global_stats[1].percentile; // GREEN + //float gamma = 255.0/global_stats[2].percentile; // BLUE + //printf("awb:%f,%f,%f\n",alfa,beta,gamma); + + } +} + + + +// Notes +/* +[1] +The packet handler thread signals end-of-frame by sending a NULL +pointer to the statistics thread. Break out of inner loop on +end-of-frame. + +[2] +This code can be written to exit early once the threshold is reached, +but that leads to a variable run time, depending upon the +characteristics of the image itself, which is highly undesirable. So, +I (astew) am writing it to have a run-time that doesn't depend on the +content of the image. + +[3] +The skewness calculation assumes the histogram has been normalized into a +probability density-like distribution whose sum across all bins is 1.0. +That is just a matter of dividing each histogram bin by the total number of +sampled pixels (which is known a priori). Because every bin is adjusted by +the same factor, we can just wait to apply the adjustment until we get +here. histogram_norm_factor is just the inverse of the total number of +sampled pixels. +*/ \ No newline at end of file diff --git a/camera/src/user_api.c b/camera/src/user_api.c new file mode 100644 index 00000000..c006bf15 --- /dev/null +++ b/camera/src/user_api.c @@ -0,0 +1,69 @@ +// std +#include +// xcore +#include +#include +// user +#include "mipi.h" +#include "utils.h" +#include "user_api.h" + +static image_t *user_image; +streaming_chanend_t c_user_api; + + +void camera_api_init( + streaming_chanend_t c_api) +{ + user_image = NULL; + c_user_api = c_api; +} + +void camera_api_request_update( + const int8_t image_row[CH][W], + const unsigned row_index) +{ + if (user_image) + { + for (int k = 0; k < CH; k++) + { + c_memcpy((void*) &user_image->pix[k][row_index][0], + (void*) &image_row[k][0], + sizeof(int8_t) * W); + } + } +} + +void camera_api_request_begin() +{ + unsigned tmp; + SELECT_RES( + CASE_THEN(c_user_api, user_handler), + DEFAULT_THEN(default_handler)) + { + user_handler: + tmp = s_chan_in_word(c_user_api); + user_image = (image_t*) tmp; + break; + default_handler: + break; + } +} + +unsigned camera_capture_image( + int8_t image_buff[CH][H][W], + streaming_chanend_t c_cam_api) +{ + int8_t *p_image = &image_buff[0][0][0]; + + s_chan_out_word(c_cam_api, (unsigned)p_image); + return s_chan_in_word(c_cam_api); +} + +void camera_api_request_complete() +{ + if(user_image){ + s_chan_out_word(c_user_api, 1); + user_image = NULL; + } +} \ No newline at end of file diff --git a/camera/src/utils.c b/camera/src/utils.c new file mode 100644 index 00000000..1b9bc6ea --- /dev/null +++ b/camera/src/utils.c @@ -0,0 +1,133 @@ + +#include +#include +#include +#include +#include + +#include "utils.h" + +// Write image to disk. This is called by camera main () to do the work +void write_image( + const char* filename, + uint8_t image[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]) +{ + printf("Writing image...\n"); + static FILE* img_file = NULL; + img_file = fopen(filename, "wb"); + + + for(uint16_t k = 0; k < APP_IMAGE_HEIGHT_PIXELS; k++){ + for(uint16_t j = 0; j < APP_IMAGE_WIDTH_PIXELS; j++){ + for(uint8_t c = 0; c < APP_IMAGE_CHANNEL_COUNT; c++){ + fwrite(&image[c][k][j], sizeof(uint8_t), 1, img_file); + } + } + } + fclose(img_file); + printf("Outfile %s\n", filename); + printf("image size (%dx%d)\n", APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_HEIGHT_PIXELS); + free(image); +} + + +// This is called when want to memcpy from Xc to C +void c_memcpy( + void* dst, + void* src, + size_t size) +{ + memcpy(dst, src, size); +} + + + +// Function to write the image data to a BMP file +void writeBMP(const char* filename, uint8_t img[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]) { + int width = APP_IMAGE_WIDTH_PIXELS; + int height = APP_IMAGE_HEIGHT_PIXELS; + int channels = APP_IMAGE_CHANNEL_COUNT; + // Define BMP file header and info header + unsigned char bmpFileHeader[14] = { + 'B', 'M', // Signature + 0, 0, 0, 0, // File size (to be filled later) + 0, 0, // Reserved + 0, 0, // Reserved + 54, 0, 0, 0 // Offset to image data + }; + + unsigned char bmpInfoHeader[40] = { + 40, 0, 0, 0, // Info header size + 0, 0, 0, 0, // Image width (to be filled later) + 0, 0, 0, 0, // Image height (to be filled later) + 1, 0, // Number of color planes + 8 * channels, 0, // Bits per pixel + 0, 0, 0, 0, // Compression method + 0, 0, 0, 0, // Image size (to be filled later) + 0, 0, 0, 0, // Horizontal resolution (pixel per meter) + 0, 0, 0, 0, // Vertical resolution (pixel per meter) + 0, 0, 0, 0, // Number of colors in the palette + 0, 0, 0, 0, // Number of important colors + }; + + // Calculate the row size (including padding) + int rowSize = width * channels; + int paddingSize = (4 - (rowSize % 4)) % 4; + int rowSizeWithPadding = rowSize + paddingSize; + + // Calculate the file size + int fileSize = 54 + (rowSizeWithPadding * height); + + // Update the file size in the BMP file header + bmpFileHeader[2] = (unsigned char)(fileSize); + bmpFileHeader[3] = (unsigned char)(fileSize >> 8); + bmpFileHeader[4] = (unsigned char)(fileSize >> 16); + bmpFileHeader[5] = (unsigned char)(fileSize >> 24); + + // Update the image width in the BMP info header + bmpInfoHeader[4] = (unsigned char)(width); + bmpInfoHeader[5] = (unsigned char)(width >> 8); + bmpInfoHeader[6] = (unsigned char)(width >> 16); + bmpInfoHeader[7] = (unsigned char)(width >> 24); + + // Update the image height in the BMP info header + bmpInfoHeader[8] = (unsigned char)(height); + bmpInfoHeader[9] = (unsigned char)(height >> 8); + bmpInfoHeader[10] = (unsigned char)(height >> 16); + bmpInfoHeader[11] = (unsigned char)(height >> 24); + + // Open the file for writing + FILE* file = fopen(filename, "wb"); + if (!file) { + printf("Error opening file for writing: %s\n", filename); + return; + } + + // Write the BMP file header and info header + fwrite(bmpFileHeader, sizeof(unsigned char), 14, file); + fwrite(bmpInfoHeader, sizeof(unsigned char), 40, file); + + // Write the image data row by row (with padding) + int i, j; + for (i = height - 1; i >= 0; i--) { + for (j = 0; j < width; j++) { + // Write the pixel data (assuming RGB order) + fwrite(&img[0][i][j], sizeof(unsigned char), 1, file); // Blue + fwrite(&img[1][i][j], sizeof(unsigned char), 1, file); // Green + fwrite(&img[2][i][j], sizeof(unsigned char), 1, file); // Red + // For 4-channel images, you can write the alpha channel here + // fwrite(&img[index + 3], sizeof(unsigned char), 1, file); // Alpha + } + + // Write the padding bytes + for (j = 0; j < paddingSize; j++) { + fwrite("\0", sizeof(unsigned char), 1, file); + } + } + + // Close the file + fclose(file); + printf("Outfile %s\n", filename); + printf("image size (%dx%d)\n", APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_HEIGHT_PIXELS); + free(img); +} diff --git a/camera/statistics.c b/camera/statistics.c deleted file mode 100644 index bb401d7a..00000000 --- a/camera/statistics.c +++ /dev/null @@ -1,155 +0,0 @@ -#include "statistics.h" - -#define BINS 64 // changing the number of bins leads to change in implementation -#define PERCENTILE_VALUE 0.05 // value selected for the percentile distribution - -#define row(x,w) (x / w) -#define col(x,w) (x % w) - -#define RED 0 -#define GREEN 1 -#define BLUE 2 - -Statistics *Statistics_alloc(void) { - Statistics *point; - point = malloc(sizeof(*point)); - if (point == NULL) { - return NULL; - } - memset(point, 0, sizeof(*point)); - return point; -} - -void Statistics_free(Statistics *self) { - free(self); -} - -Statistics Statistics_initialize(void) { - Statistics s = {{0}}; - return s; -} - -void Statistics_compute_histogram(const uint32_t buffsize, const uint8_t step, uint8_t *buffer, Statistics *statistics) -{ - // fill the histogram - for (uint32_t i=0; i< buffsize; i = i + step){ - statistics->histogram[(buffer[i] >> 2)] += 1; // because 255/4 = 64 - } - - // normalize - float inv_factor = (float)step / (float)buffsize; - for (uint8_t j=0; j < BINS; j++){ - statistics->histogram[j] *= inv_factor; - } -} - -void Statistics_compute_skewness(Statistics *statistics) -{ - const float zk_values[] = { - -1.0,-0.907753,-0.821362,-0.740633,-0.665375,-0.595396,-0.530504,-0.470508,-0.415214,-0.364431, - -0.317968,-0.275632,-0.237231,-0.202574,-0.171468,-0.143721,-0.119142,-0.097538,-0.078717,-0.062488, - -0.048659,-0.037037,-0.027431,-0.019648,-0.013497,-0.008786,-0.005323,-0.002915,-0.001372,-0.0005, - -0.000108,-4e-06,4e-06,0.000108,0.0005,0.001372,0.002915,0.005323,0.008786,0.013497, - 0.019648,0.027431,0.037037,0.048659,0.062488,0.078717,0.097538,0.119142,0.143721,0.171468, - 0.202574,0.237231,0.275632,0.317968,0.364431,0.415214,0.470508,0.530504,0.595396,0.665375, - 0.740633,0.821362,0.907753,1.0}; - - float skew = 0.0; - for (int k = 0; k < BINS; k++) - { - float pzk = statistics->histogram[k]; // we asssumed values are normalized - skew += zk_values[k] * pzk; - } - statistics -> skewness = skew; -} - - -void Statistics_compute_minmaxavg(Statistics *statistics){ - - float temp_mean = 0; - uint8_t temp_min = 0; - uint8_t temp_max = 0; - - // mean - for (uint8_t k = 1; k < BINS; k++) // k=0 does not contribute to mean value - { - temp_mean += statistics->histogram[k] * k; // assuming histogram is normalized - } - - // min - for (uint8_t k = 0; k < BINS; k++) - { - if (statistics->histogram[k]){ - temp_min = k; - break; - } - } - - // max - for (uint8_t k = BINS -1; k > 0; k--) - { - if (statistics->histogram[k]){ - temp_max = k; - break; - } - } - - // Values *4 to return to 0-255 - statistics -> mean = (uint8_t) (temp_mean+0.5) << 2; // +0.5 to avoid ceiling - statistics -> min = (uint8_t) temp_min << 2; - statistics -> max = (uint8_t) temp_max << 2; -} - -void Statistics_compute_percentile(Statistics *statistics){ - float sump = 0.0; - uint8_t k = 0; - // percentile - for (k = BINS - 1; k > 0; k--) - { - sump += statistics->histogram[k]; - if (sump > PERCENTILE_VALUE){ // I assume histogram is normalized to 0-1 - break; - } - } - statistics -> percentile = (k << 2); // Values *4 to return to 0-255 -} - -uint16_t Statistics_compute_variance(Statistics *statistics){ - uint8_t mean = statistics->mean >> 2; // /4 to return to histogram range 0-64 - float diff = 0.0; - double variance = 0; - - for (uint8_t k = 0; k < BINS; k++){ - if (statistics->histogram[k]){ - diff = (k - mean); - diff = diff*diff; - variance += statistics->histogram[k]*diff; - } - } - variance = (uint16_t)(variance * 16); // Values *16 to return to 0-255 - return variance; -} - -void Statistics_compute_all(const uint32_t buffsize, const uint8_t step, uint8_t *buffer, Statistics *statistics){ - Statistics_compute_histogram(buffsize, step, buffer, statistics); - Statistics_compute_skewness(statistics); - Statistics_compute_minmaxavg(statistics); - Statistics_compute_percentile(statistics); -} - -static char get_rgb_color(uint32_t pos, uint16_t width) { - static const char color_table[4] = {RED, GREEN, GREEN, BLUE}; - uint32_t x = pos % width; - uint32_t y = pos / width; - uint8_t index = ((y & 1) << 1) | (x & 1); - return color_table[index]; -} - - -void Statistics_print_info(Statistics *st){ - printf("min:%d, max:%d, mean:%d, percentile:%d", - st->min, - st->max, - st->mean, - st->percentile); -} \ No newline at end of file diff --git a/camera/statistics.h b/camera/statistics.h deleted file mode 100644 index 1239dc38..00000000 --- a/camera/statistics.h +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef STAtISTICS_H -#define STAtISTICS_H - -#include -#include // memset -#include // null -#include // free, alloc - -typedef struct Statistics{ - float histogram[64]; - float skewness; - uint8_t mean; - uint8_t max; - uint8_t min; - uint8_t percentile; -} Statistics; - -Statistics *Statistics_alloc(void); -Statistics Statistics_initialize(void); -void Statistics_free(Statistics *self); - -void Statistics_compute_all(const uint32_t buffsize, const uint8_t step, uint8_t *buffer, Statistics *statistics); -void Statistics_compute_histogram(const uint32_t buffsize, const uint8_t step, uint8_t *buffer, Statistics *statistics); -void Statistics_compute_skewness(Statistics *statistics); -void Statistics_compute_minmaxavg(Statistics *statistics); -void Statistics_compute_percentile(Statistics *statistics); -uint16_t Statistics_compute_variance(Statistics *statistics); - -// aux functions -void Statistics_print_info(Statistics *statistics); - -#endif \ No newline at end of file diff --git a/camera/utils.h b/camera/utils.h deleted file mode 100644 index 5605973a..00000000 --- a/camera/utils.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -#include - -#define PRINT_TIME(a,b) printf("%d\n", b - a); - -int measure_time(){ - int y = 0; - asm volatile("gettime %0": "=r"(y)); - return y; -} diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index dde51673..4cd7fecb 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -3,6 +3,5 @@ set(CONFIG_XSCOPE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) # add the examples add_subdirectory(simple_timing) -add_subdirectory(take_picture) -add_subdirectory(take_picture_dynamic) -add_subdirectory(take_picture_dynamic_crop) +add_subdirectory(take_picture_raw) +add_subdirectory(take_picture_downsample) diff --git a/examples/take_picture_dynamic/CMakeLists.txt b/examples/take_picture_downsample/CMakeLists.txt similarity index 93% rename from examples/take_picture_dynamic/CMakeLists.txt rename to examples/take_picture_downsample/CMakeLists.txt index 4a123287..8660d172 100644 --- a/examples/take_picture_dynamic/CMakeLists.txt +++ b/examples/take_picture_downsample/CMakeLists.txt @@ -3,7 +3,7 @@ #********************** # <--- Set the executable -set(TARGET example_take_picture_dynamic) +set(TARGET example_take_picture_downsample) file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.*c) set(APP_INCLUDES @@ -25,6 +25,7 @@ set(APP_COMPILER_FLAGS ${CONFIG_XSCOPE_PATH}/config.xscope ) + set(APP_COMPILE_DEFINITIONS configENABLE_DEBUG_PRINTF=1 PLATFORM_SUPPORTS_TILE_0=1 diff --git a/examples/take_picture/README.md b/examples/take_picture_downsample/README.md similarity index 100% rename from examples/take_picture/README.md rename to examples/take_picture_downsample/README.md diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c new file mode 100644 index 00000000..04820072 --- /dev/null +++ b/examples/take_picture_downsample/src/app.c @@ -0,0 +1,49 @@ + +#include "app.h" + +#include +#include + +void user_app(streaming_chanend_t c_cam_api){ + int8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; + uint8_t out_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; + + for(int8_t k = 0; k < APP_IMAGE_CHANNEL_COUNT; k++){ + memset(&image_buffer[k][0][0], -128, APP_IMAGE_HEIGHT_PIXELS * APP_IMAGE_WIDTH_PIXELS); // important to set to 0 in int8 (-128) + } + + delay_milliseconds(5000); + printf("Requesting image...\n"); + camera_capture_image(image_buffer, c_cam_api); + printf("Image captured...\n"); + + // Add 128 to all elements + for(uint16_t c = 0; c < APP_IMAGE_CHANNEL_COUNT; c++){ + for(uint16_t k = 0; k < APP_IMAGE_HEIGHT_PIXELS; k++){ + for(uint16_t j = 0; j < APP_IMAGE_WIDTH_PIXELS; j++){ + out_buffer[c][k][j] = (uint8_t)(image_buffer[c][k][j] + 128); + } + } + } + + // Rotate 180 degrees + #if ROTATE_IMAGE + for(int c = 0; c < APP_IMAGE_CHANNEL_COUNT; c++) { + for(int k = 0; k < APP_IMAGE_HEIGHT_PIXELS/2; k++) { + for(int j = 0; j < APP_IMAGE_WIDTH_PIXELS; j++) { + uint8_t a = image_buffer[c][k][j]; + uint8_t b = image_buffer[c][APP_IMAGE_HEIGHT_PIXELS-k-1][APP_IMAGE_WIDTH_PIXELS-j-1]; + image_buffer[c][k][j] = b; + image_buffer[c][APP_IMAGE_HEIGHT_PIXELS-k-1][APP_IMAGE_WIDTH_PIXELS-j-1] = a; + } + } + } + #endif + + // Write binary file and .bmp file + write_image("capture.bin", out_buffer); + writeBMP("capture.bmp", out_buffer); + + // end here + exit(0); +} diff --git a/examples/take_picture_downsample/src/app.h b/examples/take_picture_downsample/src/app.h new file mode 100644 index 00000000..0c6ca9b1 --- /dev/null +++ b/examples/take_picture_downsample/src/app.h @@ -0,0 +1,10 @@ +#include "xs1.h" +#include "platform.h" +#include "xccompat.h" + +#include "camera.h" + + +#define ROTATE_IMAGE 0 + +void user_app(streaming_chanend_t c_cam_api); \ No newline at end of file diff --git a/examples/take_picture_downsample/src/main.xc b/examples/take_picture_downsample/src/main.xc new file mode 100644 index 00000000..be13aa72 --- /dev/null +++ b/examples/take_picture_downsample/src/main.xc @@ -0,0 +1,46 @@ +// Copyright (c) 2020, XMOS Ltd, All rights reserved +#include +#include +#include + +#include "i2c.h" +#include "camera.h" +#include "app.h" + +// I2C interface ports +#define Kbps 400 +on tile[0]: port p_scl = XS1_PORT_1N; +on tile[0]: port p_sda = XS1_PORT_1O; + + +/** +* Declaration of the MIPI interface ports: +* Clock, receiver active, receiver data valid, and receiver data +*/ +on tile[MIPI_TILE] : in port p_mipi_clk = XS1_PORT_1O; +on tile[MIPI_TILE] : in port p_mipi_rxa = XS1_PORT_1E; // activate +on tile[MIPI_TILE] : in port p_mipi_rxv = XS1_PORT_1I; // valid +on tile[MIPI_TILE] : buffered in port:32 p_mipi_rxd = XS1_PORT_8A; // data +on tile[MIPI_TILE] : clock clk_mipi = MIPI_CLKBLK; + + +int main(void) +{ + streaming chan c_cam_api; + i2c_master_if i2c[1]; + par { + on tile[0]: i2c_master(i2c, 1, p_scl, p_sda, Kbps); + + on tile[MIPI_TILE]: camera_main(tile[MIPI_TILE], + p_mipi_clk, + p_mipi_rxa, + p_mipi_rxv, + p_mipi_rxd, + clk_mipi, + i2c[0], + c_cam_api); + + on tile[MIPI_TILE]: user_app(c_cam_api); + } + return 0; +} diff --git a/examples/take_picture_dynamic/src/main.xc b/examples/take_picture_dynamic/src/main.xc deleted file mode 100644 index 47a72322..00000000 --- a/examples/take_picture_dynamic/src/main.xc +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2020, XMOS Ltd, All rights reserved -#include -#include -#include - -#include "i2c.h" -#include "mipi_main.h" - -// I2C interface ports -#define Kbps 400 -on tile[0]: port p_scl = XS1_PORT_1N; -on tile[0]: port p_sda = XS1_PORT_1O; - -int main(void) -{ - i2c_master_if i2c[1]; - par { - on tile[0]: i2c_master(i2c, 1, p_scl, p_sda, Kbps); - on tile[MIPI_TILE]: mipi_main(i2c[0]); - } - return 0; -} diff --git a/examples/take_picture_dynamic/src/mipi_main.h b/examples/take_picture_dynamic/src/mipi_main.h deleted file mode 100644 index 788c849b..00000000 --- a/examples/take_picture_dynamic/src/mipi_main.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include -#include "mipi.h" - -#include "sensor.h" - -///TODO this is inside lib_mipi -#ifndef MIPI_CLKBLK -#define MIPI_CLKBLK XS1_CLKBLK_1 -#endif - -/** - * The packet buffer is where the packet decoupler will tell the MIPI receiver - * thread to store received packets. - */ -#define DEMUX_DATATYPE 0 // RESERVED -#define DEMUX_MODE 0x00 // no demux -#define DEMUX_EN 0 // DISABLE DEMUX -#define MIPI_CLK_DIV 1 // CLK DIVIDER -#define MIPI_CFG_CLK_DIV 3 // CFG DIVIDER -#define REV(n) ((n << 24) | (((n>>16)<<24)>>16) | (((n<<16)>>24)<<16) | (n>>24)) - -// Packets definitions -typedef struct -{ - mipi_header_t header; - uint8_t payload[MIPI_LINE_WIDTH_BYTES]; -} mipi_data_t; - -typedef struct -{ - mipi_header_t header; - uint8_t payload[MIPI_MAX_PKT_SIZE_BYTES]; -} mipi_packet_t; - -typedef struct -{ - unsigned frame_number; - unsigned line_number; -} image_rx_t; - -static mipi_packet_t packet_buffer[MIPI_PKT_BUFFER_COUNT]; - -#ifdef __XC__ - -void mipi_main(client interface i2c_master_if i2c); - -#endif diff --git a/examples/take_picture_dynamic/src/mipi_main.xc b/examples/take_picture_dynamic/src/mipi_main.xc deleted file mode 100644 index 6f928087..00000000 --- a/examples/take_picture_dynamic/src/mipi_main.xc +++ /dev/null @@ -1,248 +0,0 @@ -#include -#include -#include -#include // for ports -#include -#include // exit status and malloc -#include -#include -#include - -#include - -// I2C -#include "i2c.h" - -// MIPI -#include "mipi_main.h" - -// Sensor -#define MSG_SUCCESS "Stream start OK\n" -#define MSG_FAIL "Stream start Failed\n" - -// Image -#include "process_frame.h" - -// Globals -char end_transmission = 0; - -typedef chanend chanend_t; - - -/** -* Declaration of the MIPI interface ports: -* Clock, receiver active, receiver data valid, and receiver data -*/ -on tile[MIPI_TILE] : in port p_mipi_clk = XS1_PORT_1O; -on tile[MIPI_TILE] : in port p_mipi_rxa = XS1_PORT_1E; // activate -on tile[MIPI_TILE] : in port p_mipi_rxv = XS1_PORT_1I; // valid -on tile[MIPI_TILE] : buffered in port:32 p_mipi_rxd = XS1_PORT_8A; // data -on tile[MIPI_TILE] : clock clk_mipi = MIPI_CLKBLK; - - -extern "C" -{ - void process_image(uint8_t *image, chanend_t c); -} - - -void control_exposure(chanend_t ch, client interface i2c_master_if i2c){ - while(1){ - select { - case ch :> unsigned exposure: - //printf("Exposure :> %d\n", exposure); - imx219_set_gain_dB(i2c, exposure); - break ; - } - } -} - - - - -unsafe { - -static -void handle_packet( - image_rx_t* img_rx, - const mipi_packet_t* unsafe pkt, - chanend flag, - uint8_t* unsafe img_raw_ptr, - chanend_t ch_exp) - { - // End here if just one transmission - //if (end_transmission == 1){ - // return; - //} - - // definitions - const mipi_header_t header = pkt->header; - const mipi_data_type_t data_type = MIPI_GET_DATA_TYPE(header); - const unsigned is_long = MIPI_IS_LONG_PACKET(header); // not used for the moment - const unsigned word_count = MIPI_GET_WORD_COUNT(header); // not used for the moment - static uint8_t wait_for_clean_frame = 1; // static because it will change in the future - // printf("packet header = 0x%08x, wc=%d \n", REV(header), word_count); - - // We return until the start of frame is reached - if (wait_for_clean_frame == 1){ - if (data_type != MIPI_DT_FRAME_START){ - return; - } - else{ - wait_for_clean_frame = 0; - } - } - //printf("-- Im here 4--\n"); - // printf("dat type: 0x%08x\n",data_type); - // Use case by data type - switch (data_type) - { - case MIPI_DT_FRAME_START: { // Start of frame. Just reset line number. - img_rx->frame_number++; - img_rx->line_number = 0; - break; - } - - case EXPECTED_FORMAT:{ // save it in SRAM and increment line - // if line number is grater than expected, just reset the line number - if (img_rx->line_number >= MIPI_IMAGE_HEIGHT_PIXELS){ - break; // let pass the rest until next frame - } - // then copy - uint32_t pos = (img_rx->line_number) * MIPI_LINE_WIDTH_BYTES; - // *** horizontal / vertical filtering **** - - - // ******************************* - c_memcpy(&img_raw_ptr[pos], &pkt->payload[0], MIPI_LINE_WIDTH_BYTES); - // increase line number - img_rx->line_number++; - break; - } - - case MIPI_DT_FRAME_END:{ // we signal that the frame is finish so we can write it to a file - //if (end_transmission == 0){ //TODO not needed if and the end of transmission I just return - - //flag <: 1; - // wait_for_clean_frame = 1; // in case - // end_transmission = 1; - //} - process_image(img_raw_ptr, ch_exp); - //printf("-- Im here 3--\n"); - break; - } - - default:{ // error with frame type or protected types or blank fields - break; - } - } - } - -#pragma unsafe arrays -static void mipi_packet_handler( - streaming chanend c_pkt, - streaming chanend c_ctrl, - chanend flag, - uint8_t * unsafe image_ptr, - chanend_t ch_exp) -{ - image_rx_t img_rx = {0, 0}; // stores the coordinates X, Y of the image - unsigned pkt_idx = 0; // packet index - - // Give the MIPI packet receiver a buffer - outuint((chanend)c_pkt, (unsigned)&packet_buffer[pkt_idx]); - pkt_idx = (pkt_idx + 1) & (MIPI_PKT_BUFFER_COUNT - 1); - - while (1) - { - // Wait for the receiver thread to tell us a new packet was completed. - mipi_packet_t *unsafe pkt = (mipi_packet_t * unsafe) inuint((chanend)c_pkt); - - // Give it a new buffer before processing the received one - outuint((chanend)c_pkt, (unsigned)&packet_buffer[pkt_idx]); - pkt_idx = (pkt_idx + 1) & (MIPI_PKT_BUFFER_COUNT - 1); - - // Process the packet. We need to be finished with this and looped - // back up to grab the next MIPI packet BEFORE the receiver thread - // tries to give us the next packet. - handle_packet(&img_rx, pkt, flag, image_ptr, ch_exp); - - //if (end_transmission == 1) - //{ - // return; - //} - } -} - -} // unsafe region - - -void mipi_main(client interface i2c_master_if i2c) -{ - printf("< Start of APP capture application >\n"); - streaming chan c_pkt; - streaming chan c_ctrl; - chan flag; - chan ch_exp; - - // allocate espace for the image buffer - uint8_t* unsafe img_raw_ptr = malloc(MIPI_IMAGE_HEIGHT_PIXELS * MIPI_IMAGE_WIDTH_PIXELS * sizeof(uint8_t)); - - // See AN for MIPI shim - // 0x7E42 >> 0111 1110 0100 0010 - // in the explorer BOARD DPDN is swap - write_node_config_reg(tile[MIPI_TILE], - XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, - 0x7E42); //TODO decompose into different values - - // send packet to MIPI shim - MipiPacketRx_init(tile[MIPI_TILE], - p_mipi_rxd, - p_mipi_rxv, - p_mipi_rxa, - p_mipi_clk, - clk_mipi, - DEMUX_EN, - DEMUX_DATATYPE, - DEMUX_MODE, - MIPI_CLK_DIV, - MIPI_CFG_CLK_DIV); - - // Start camera and its configurations - int r = 0; - r |= camera_init(i2c); - delay_milliseconds(100); //TODO include this inside the function - r |= camera_configure(i2c); - delay_milliseconds(500); - r |= camera_start(i2c); - delay_milliseconds(2000); - - if (r != 0){ - printf(MSG_FAIL); - } - else{ - printf(MSG_SUCCESS); - } - - // start the different jobs (packet controller, handler, and post_process) - par - { - MipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); - mipi_packet_handler(c_pkt, c_ctrl, flag, img_raw_ptr, ch_exp); - //save_image_to_file(flag, img_raw_ptr); - control_exposure(ch_exp, i2c); - } -} - - -/* - //uint16_t val = imx219_read(i2c, 0x0174); - //printf("read value = %d\n", val); - - uint16_t gain_values[5]; - imx219_read_gains(i2c, gain_values); - for (int i = 0; i < 5; i++) - { - printf("gain value = %d\n", gain_values[i]); - } -*/ \ No newline at end of file diff --git a/examples/take_picture_dynamic/src/process_frame.c b/examples/take_picture_dynamic/src/process_frame.c deleted file mode 100644 index 4b0a36b3..00000000 --- a/examples/take_picture_dynamic/src/process_frame.c +++ /dev/null @@ -1,87 +0,0 @@ -#include -#include -#include -#include - -#include - -#include "process_frame.h" -#include "statistics.h" // for skewness and -#include "utils.h" // for measuring time -#include "isp.h" // setting auto_exposure, AWB - -#define FINAL_IMAGE_FILENAME "img_raw.bin" - - -const uint32_t img_len = MIPI_LINE_WIDTH_BYTES*MIPI_IMAGE_HEIGHT_PIXELS; -float new_exp = 35; -Statistics st = {{0}}; - -// Write image to disk. This is called by camera main () to do the work -void write_image(uint8_t *image) -{ - static FILE* img_file = NULL; - img_file = fopen(FINAL_IMAGE_FILENAME, "wb"); - for(uint16_t k = 0; k < MIPI_IMAGE_HEIGHT_PIXELS; k++){ - for(uint16_t j = 0; j < MIPI_IMAGE_WIDTH_BYTES; j++){ - uint32_t pos = k * MIPI_LINE_WIDTH_BYTES + j; - fwrite(&image[pos], sizeof(uint8_t), 1, img_file); - } - } - fclose(img_file); - printf("Outfile %s\n", FINAL_IMAGE_FILENAME); - printf("image size (%dx%d)\n", MIPI_LINE_WIDTH_BYTES, MIPI_IMAGE_HEIGHT_PIXELS); - free(image); - exit(1); -} - - - -void process_image(uint8_t *image, chanend_t c){ - static int initial_t = 0; - if (initial_t == 0){ - initial_t = measure_time(); - } - static int print_msg = 0; - - // compute statistics - Statistics_compute_all(img_len, STEP, image, (Statistics *) &st); - float sk = st.skewness; - - // print information - Statistics_print_info(&st); - printf("exposure:%f, skewness:%f\n", new_exp, sk); - - // exit condition - if (sk < AE_MARGIN && sk > -AE_MARGIN){ - if (print_msg == 0){ - printf("-----> adjustement done\n"); - print_msg = 1; - } - } - else{ - // adjust exposure - new_exp = isp_false_position_step(new_exp, sk); - - // put exposure - #if ENABLE_AE - chan_out_word(c, (unsigned) new_exp); - #endif - // cancel output message - print_msg = 0; - } - - // write it to a file - if (measure_time() - initial_t >= 600000000){ - write_image(image); - } -} - -// This is called when want to memcpy from Xc to C -void c_memcpy( - void* dst, - void* src, - size_t size) -{ - memcpy(dst, src, size); -} diff --git a/examples/take_picture_dynamic/src/process_frame.h b/examples/take_picture_dynamic/src/process_frame.h deleted file mode 100644 index e17714e9..00000000 --- a/examples/take_picture_dynamic/src/process_frame.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include -#include "mipi_main.h" - - -#ifdef __XC__ -extern "C" { -#endif - -void write_image(uint8_t *image); -void c_memcpy(void *dst, void *src, size_t size); -//void process_image(uint8_t *image, chanend_t c); - - -// void user_input(void); - -#ifdef __XC__ -} -#endif \ No newline at end of file diff --git a/examples/take_picture_dynamic_crop/CMakeLists.txt b/examples/take_picture_dynamic_crop/CMakeLists.txt deleted file mode 100644 index f7672807..00000000 --- a/examples/take_picture_dynamic_crop/CMakeLists.txt +++ /dev/null @@ -1,64 +0,0 @@ -#********************** -# Gather Sources -#********************** - -# <--- Set the executable -set(TARGET example_take_picture_crop) - -file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.*c) -set(APP_INCLUDES - ${CMAKE_CURRENT_LIST_DIR}/src -) - - -#********************** -# Flags -#********************** -set(APP_COMPILER_FLAGS - -Os - -g - -report - -fxscope - -mcmodel=large - -Wno-xcore-fptrgroup - -target=${XCORE_TARGET} - ${CONFIG_XSCOPE_PATH}/config.xscope -) - -set(APP_COMPILE_DEFINITIONS - configENABLE_DEBUG_PRINTF=1 - PLATFORM_SUPPORTS_TILE_0=1 - PLATFORM_SUPPORTS_TILE_1=1 - PLATFORM_SUPPORTS_TILE_2=0 - PLATFORM_SUPPORTS_TILE_3=0 - PLATFORM_USES_TILE_0=1 - PLATFORM_USES_TILE_1=1 - XUD_CORE_CLOCK=600 -) - -set(APP_LINK_OPTIONS - "-report" - "-target=${XCORE_TARGET}" - "${CONFIG_XSCOPE_PATH}/config.xscope" -) - -# <--- Link libraries -set(APP_COMMON_LINK_LIBRARIES - core::general - mipi::lib_mipi - i2c::lib_i2c - sensors::lib_imx - camera::lib_camera - ) - - -#********************** -# Tile Targets -#********************** -add_executable(${TARGET} EXCLUDE_FROM_ALL) -target_sources(${TARGET} PUBLIC ${APP_SOURCES}) -target_include_directories(${TARGET} PUBLIC ${APP_INCLUDES}) -target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) -target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) -target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) -target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) diff --git a/examples/take_picture_dynamic_crop/src/main.xc b/examples/take_picture_dynamic_crop/src/main.xc deleted file mode 100644 index 47a72322..00000000 --- a/examples/take_picture_dynamic_crop/src/main.xc +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2020, XMOS Ltd, All rights reserved -#include -#include -#include - -#include "i2c.h" -#include "mipi_main.h" - -// I2C interface ports -#define Kbps 400 -on tile[0]: port p_scl = XS1_PORT_1N; -on tile[0]: port p_sda = XS1_PORT_1O; - -int main(void) -{ - i2c_master_if i2c[1]; - par { - on tile[0]: i2c_master(i2c, 1, p_scl, p_sda, Kbps); - on tile[MIPI_TILE]: mipi_main(i2c[0]); - } - return 0; -} diff --git a/examples/take_picture_dynamic_crop/src/mipi_main.h b/examples/take_picture_dynamic_crop/src/mipi_main.h deleted file mode 100644 index 788c849b..00000000 --- a/examples/take_picture_dynamic_crop/src/mipi_main.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include -#include "mipi.h" - -#include "sensor.h" - -///TODO this is inside lib_mipi -#ifndef MIPI_CLKBLK -#define MIPI_CLKBLK XS1_CLKBLK_1 -#endif - -/** - * The packet buffer is where the packet decoupler will tell the MIPI receiver - * thread to store received packets. - */ -#define DEMUX_DATATYPE 0 // RESERVED -#define DEMUX_MODE 0x00 // no demux -#define DEMUX_EN 0 // DISABLE DEMUX -#define MIPI_CLK_DIV 1 // CLK DIVIDER -#define MIPI_CFG_CLK_DIV 3 // CFG DIVIDER -#define REV(n) ((n << 24) | (((n>>16)<<24)>>16) | (((n<<16)>>24)<<16) | (n>>24)) - -// Packets definitions -typedef struct -{ - mipi_header_t header; - uint8_t payload[MIPI_LINE_WIDTH_BYTES]; -} mipi_data_t; - -typedef struct -{ - mipi_header_t header; - uint8_t payload[MIPI_MAX_PKT_SIZE_BYTES]; -} mipi_packet_t; - -typedef struct -{ - unsigned frame_number; - unsigned line_number; -} image_rx_t; - -static mipi_packet_t packet_buffer[MIPI_PKT_BUFFER_COUNT]; - -#ifdef __XC__ - -void mipi_main(client interface i2c_master_if i2c); - -#endif diff --git a/examples/take_picture_dynamic_crop/src/mipi_main.xc b/examples/take_picture_dynamic_crop/src/mipi_main.xc deleted file mode 100644 index 34ab1fe9..00000000 --- a/examples/take_picture_dynamic_crop/src/mipi_main.xc +++ /dev/null @@ -1,253 +0,0 @@ -#include -#include -#include -#include // for ports -#include -#include // exit status and malloc -#include -#include -#include - -#include - -// I2C -#include "i2c.h" - -// MIPI -#include "mipi_main.h" - -// Sensor -#define MSG_SUCCESS "Stream start OK\n" -#define MSG_FAIL "Stream start Failed\n" - -// Image -#include "process_frame.h" - -// Globals -char end_transmission = 0; - -typedef chanend chanend_t; - - -/** -* Declaration of the MIPI interface ports: -* Clock, receiver active, receiver data valid, and receiver data -*/ -on tile[MIPI_TILE] : in port p_mipi_clk = XS1_PORT_1O; -on tile[MIPI_TILE] : in port p_mipi_rxa = XS1_PORT_1E; // activate -on tile[MIPI_TILE] : in port p_mipi_rxv = XS1_PORT_1I; // valid -on tile[MIPI_TILE] : buffered in port:32 p_mipi_rxd = XS1_PORT_8A; // data -on tile[MIPI_TILE] : clock clk_mipi = MIPI_CLKBLK; - - -extern "C" -{ - void process_image(uint8_t *image, chanend_t c); -} - - -void control_exposure(chanend_t ch, client interface i2c_master_if i2c){ - while(1){ - select { - case ch :> unsigned exposure: - //printf("Exposure :> %d\n", exposure); - imx219_set_gain_dB(i2c, exposure); - break ; - } - } -} - - - - -unsafe { - -static -void handle_packet( - image_rx_t* img_rx, - const mipi_packet_t* unsafe pkt, - uint8_t* unsafe img_raw_ptr, - chanend_t ch_exp) - { - // End here if just one transmission - //if (end_transmission == 1){ - // return; - //} - - // definitions - const mipi_header_t header = pkt->header; - const mipi_data_type_t data_type = MIPI_GET_DATA_TYPE(header); - const unsigned is_long = MIPI_IS_LONG_PACKET(header); // not used for the moment - const unsigned word_count = MIPI_GET_WORD_COUNT(header); // not used for the moment - static uint8_t wait_for_clean_frame = 1; // static because it will change in the future - // printf("packet header = 0x%08x, wc=%d \n", REV(header), word_count); - - // We return until the start of frame is reached - if (wait_for_clean_frame == 1){ - if (data_type != MIPI_DT_FRAME_START){ - return; - } - else{ - wait_for_clean_frame = 0; - } - } - //printf("-- Im here 4--\n"); - // printf("dat type: 0x%08x\n",data_type); - // Use case by data type - switch (data_type) - { - case MIPI_DT_FRAME_START: { // Start of frame. Just reset line number. - img_rx->frame_number++; - img_rx->line_number = 0; - break; - } - - case EXPECTED_FORMAT:{ // save it in SRAM and increment line - // if line number is grater than expected, just reset the line number - if (img_rx->line_number >= MIPI_IMAGE_HEIGHT_PIXELS){ - break; // let pass the rest until next frame - } - - #if (CROP_ENABLED) - // check if y axis in range - if (img_rx->line_number >= Y_START_CROP){ - if (img_rx->line_number < (Y_START_CROP + CROP_HEIGHT_PIXELS)){ - // compute x axis - uint32_t pos = ((img_rx->line_number) - Y_START_CROP)* CROP_WIDTH_PIXELS; - // printf("pos = %d\n", pos); - c_memcpy(&img_raw_ptr[pos], &pkt->payload[X_START_CROP], CROP_WIDTH_PIXELS); - } - } - #else - uint32_t pos = (img_rx->line_number) * MIPI_LINE_WIDTH_BYTES; - c_memcpy(&img_raw_ptr[pos], &pkt->payload[0], MIPI_LINE_WIDTH_BYTES); - #endif - - // increase line number - img_rx->line_number++; - break; - } - - case MIPI_DT_FRAME_END:{ // we signal that the frame is finish so we can write it to a file - //process_image(img_raw_ptr, ch_exp); - write_image(img_raw_ptr); - break; - } - - default:{ // error with frame type or protected types or blank fields - break; - } - } - } - -#pragma unsafe arrays -static void mipi_packet_handler( - streaming chanend c_pkt, - streaming chanend c_ctrl, - uint8_t * unsafe image_ptr, - chanend_t ch_exp) -{ - image_rx_t img_rx = {0, 0}; // stores the coordinates X, Y of the image - unsigned pkt_idx = 0; // packet index - - // Give the MIPI packet receiver a buffer - outuint((chanend)c_pkt, (unsigned)&packet_buffer[pkt_idx]); - pkt_idx = (pkt_idx + 1) & (MIPI_PKT_BUFFER_COUNT - 1); - - while (1) - { - // Wait for the receiver thread to tell us a new packet was completed. - mipi_packet_t *unsafe pkt = (mipi_packet_t * unsafe) inuint((chanend)c_pkt); - - // Give it a new buffer before processing the received one - outuint((chanend)c_pkt, (unsigned)&packet_buffer[pkt_idx]); - pkt_idx = (pkt_idx + 1) & (MIPI_PKT_BUFFER_COUNT - 1); - - // Process the packet. We need to be finished with this and looped - // back up to grab the next MIPI packet BEFORE the receiver thread - // tries to give us the next packet. - handle_packet(&img_rx, pkt, image_ptr, ch_exp); - - //if (end_transmission == 1) - //{ - // return; - //} - } -} - -} // unsafe region - - -void mipi_main(client interface i2c_master_if i2c) -{ - printf("< Start of APP capture application >\n"); - streaming chan c_pkt; - streaming chan c_ctrl; - //chan flag; - chan ch_exp; - - // allocate espace for the image buffer - #if (CROP_ENABLED) - uint8_t* unsafe img_raw_ptr = malloc(CROP_WIDTH_PIXELS * CROP_HEIGHT_PIXELS * sizeof(uint8_t)); - #else - uint8_t* unsafe img_raw_ptr = malloc(MIPI_IMAGE_HEIGHT_PIXELS * MIPI_IMAGE_WIDTH_PIXELS * sizeof(uint8_t)); - #endif - - // See AN for MIPI shim - // 0x7E42 >> 0111 1110 0100 0010 - // in the explorer BOARD DPDN is swap - write_node_config_reg(tile[MIPI_TILE], - XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, - 0x7E42); //TODO decompose into different values - - // send packet to MIPI shim - MipiPacketRx_init(tile[MIPI_TILE], - p_mipi_rxd, - p_mipi_rxv, - p_mipi_rxa, - p_mipi_clk, - clk_mipi, - DEMUX_EN, - DEMUX_DATATYPE, - DEMUX_MODE, - MIPI_CLK_DIV, - MIPI_CFG_CLK_DIV); - - // Start camera and its configurations - int r = 0; - r |= camera_init(i2c); - delay_milliseconds(100); //TODO include this inside the function - r |= camera_configure(i2c); - delay_milliseconds(500); - r |= camera_start(i2c); - delay_milliseconds(2000); - - if (r != 0){ - printf(MSG_FAIL); - } - else{ - printf(MSG_SUCCESS); - } - - // start the different jobs (packet controller, handler, and post_process) - par - { - MipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); - mipi_packet_handler(c_pkt, c_ctrl, img_raw_ptr, ch_exp); - //save_image_to_file(flag, img_raw_ptr); - control_exposure(ch_exp, i2c); - } -} - - -/* - //uint16_t val = imx219_read(i2c, 0x0174); - //printf("read value = %d\n", val); - - uint16_t gain_values[5]; - imx219_read_gains(i2c, gain_values); - for (int i = 0; i < 5; i++) - { - printf("gain value = %d\n", gain_values[i]); - } -*/ \ No newline at end of file diff --git a/examples/take_picture_dynamic_crop/src/process_frame.c b/examples/take_picture_dynamic_crop/src/process_frame.c deleted file mode 100644 index ce3028ab..00000000 --- a/examples/take_picture_dynamic_crop/src/process_frame.c +++ /dev/null @@ -1,96 +0,0 @@ -#include -#include -#include -#include - -#include - -#include "process_frame.h" -#include "statistics.h" // for skewness and -#include "utils.h" // for measuring time -#include "isp.h" // setting auto_exposure, AWB - -#define FINAL_IMAGE_FILENAME "img_raw.bin" - -#if (CROP_ENABLED) - #define IMG_W CROP_WIDTH_PIXELS - #define IMG_H CROP_HEIGHT_PIXELS -#else - #define IMG_W MIPI_LINE_WIDTH_BYTES - #define IMG_H MIPI_IMAGE_HEIGHT_PIXELS -#endif - -// Global definitions -const uint32_t img_len = IMG_W*IMG_H; -float new_exp = 35; -Statistics st = {{0}}; - -// Write image to disk. This is called by camera main () to do the work -void write_image(uint8_t *image) -{ - static FILE* img_file = NULL; - img_file = fopen(FINAL_IMAGE_FILENAME, "wb"); - for(uint16_t k = 0; k < IMG_H; k++){ - for(uint16_t j = 0; j < IMG_W; j++){ - uint32_t pos = k * IMG_W + j; - fwrite(&image[pos], sizeof(uint8_t), 1, img_file); - } - } - fclose(img_file); - printf("Outfile %s\n", FINAL_IMAGE_FILENAME); - printf("image size (%dx%d)\n", IMG_W, IMG_H); - free(image); - yuv_to_rgb(2,2,2); - exit(1); -} - - - -void process_image(uint8_t *image, chanend_t c){ - static int initial_t = 0; - if (initial_t == 0){ - initial_t = measure_time(); - } - static int print_msg = 0; - - // compute statistics - Statistics_compute_all(img_len, STEP, image, (Statistics *) &st); - float sk = st.skewness; - - // print information - printf("min:%d, max:%d, mean:%d, percentile:%d, exposure:%f, skewness:%f\n", - st.min, st.max, st.mean, st.percentile, new_exp, sk); - - // exit condition - if (sk < AE_MARGIN && sk > -AE_MARGIN){ - if (print_msg == 0){ - printf("-----> adjustement done\n"); - print_msg = 1; - } - } - else{ - // adjust exposure - new_exp = isp_false_position_step(new_exp, sk); - - // put exposure - #if ENABLE_AE - chan_out_word(c, (unsigned) new_exp); - #endif - // cancel output message - print_msg = 0; - } - - // write it to a file - if (measure_time() - initial_t >= 600000000){ - write_image(image); - } -} - -// This is called when want to memcpy from Xc to C -void c_memcpy( - void* dst, - void* src, - size_t size) -{ - memcpy(dst, src, size); -} diff --git a/examples/take_picture_dynamic_crop/src/process_frame.h b/examples/take_picture_dynamic_crop/src/process_frame.h deleted file mode 100644 index e17714e9..00000000 --- a/examples/take_picture_dynamic_crop/src/process_frame.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include -#include "mipi_main.h" - - -#ifdef __XC__ -extern "C" { -#endif - -void write_image(uint8_t *image); -void c_memcpy(void *dst, void *src, size_t size); -//void process_image(uint8_t *image, chanend_t c); - - -// void user_input(void); - -#ifdef __XC__ -} -#endif \ No newline at end of file diff --git a/examples/take_picture/CMakeLists.txt b/examples/take_picture_raw/CMakeLists.txt similarity index 100% rename from examples/take_picture/CMakeLists.txt rename to examples/take_picture_raw/CMakeLists.txt diff --git a/examples/take_picture_raw/README.md b/examples/take_picture_raw/README.md new file mode 100644 index 00000000..cf6ae175 --- /dev/null +++ b/examples/take_picture_raw/README.md @@ -0,0 +1,13 @@ +## Example: Take picture + +This example set the basic settings for the sony sensor and grab a single frame. +By default the format is the following: +* 640x480 RAW8 + +### File Description +* Sensor.h : configures and set the basic settings for the sensor +* Main.xc : starts 2 threads, i2C control and mipi capture +* Process_frame.c : write the image to a file + +## Build example +Run the following command: ```make example_take_picture``` \ No newline at end of file diff --git a/examples/take_picture/src/main.xc b/examples/take_picture_raw/src/main.xc similarity index 100% rename from examples/take_picture/src/main.xc rename to examples/take_picture_raw/src/main.xc diff --git a/examples/take_picture/src/mipi_main.h b/examples/take_picture_raw/src/mipi_main.h similarity index 100% rename from examples/take_picture/src/mipi_main.h rename to examples/take_picture_raw/src/mipi_main.h diff --git a/examples/take_picture/src/mipi_main.xc b/examples/take_picture_raw/src/mipi_main.xc similarity index 100% rename from examples/take_picture/src/mipi_main.xc rename to examples/take_picture_raw/src/mipi_main.xc diff --git a/examples/take_picture/src/process_frame.c b/examples/take_picture_raw/src/process_frame.c similarity index 100% rename from examples/take_picture/src/process_frame.c rename to examples/take_picture_raw/src/process_frame.c diff --git a/examples/take_picture/src/process_frame.h b/examples/take_picture_raw/src/process_frame.h similarity index 100% rename from examples/take_picture/src/process_frame.h rename to examples/take_picture_raw/src/process_frame.h diff --git a/modules/mipi/src/MipiPacketRx.S b/modules/mipi/src/MipiPacketRx.S index beceaa20..265e6694 100644 --- a/modules/mipi/src/MipiPacketRx.S +++ b/modules/mipi/src/MipiPacketRx.S @@ -1,284 +1,294 @@ - -#include -#include - - -/**************************************************** - **************************************************** - -Then to receive a packet: - -- Load context data from global memory (RxA, RxD) -- If RxA is high, wait for it to go low. (so we don't start mid-packet) - - Initial version can just do this with polling so we don't need to do any - weird jiggery pokery with the RxA low event vector (which we're already - using for a different purpose). -- Once RxA is low, use IN to wait for the first word of the packet -- Put the first word in the buffer -- If it's a short packet, return - - Maybe it's a good idea to return the data type? -- Otherwse, do the data loop -- Return - - Maybe it's a good idea to return the data type? - - **************************************************** - **************************************************** - */ - -/* - See MIPI CSI section 9.1.2 - - typedef struct { - uint8_t data_id; - uint16_t word_count; // In MIPI a word is a byte - uint8_t vcx_ecc; - } mipi_packet_header_t; - - Note: When that 32-bit header is read in on the buffered port, the data_id - will be the *least* significant 8 bits because of the direction of this port's - shift register. - - Bits: - data_id[7:6] - least significant two bits of 4-bit Virtual Channel ID - data_id[5:0] - data_type - vcx_ecc[7:6] - most significant two bits of 4-bit Virtual Channel ID - vcx_ecc[5:0] - error correction code -*/ - -.issue_mode dual - -#define FUNCTION_NAME MipiPacketRx -#define NSTACKWORDS 8 - -.globl FUNCTION_NAME.nstackwords -.globl FUNCTION_NAME.maxthreads -.globl FUNCTION_NAME.maxtimers -.globl FUNCTION_NAME.maxchanends - -.linkset FUNCTION_NAME.nstackwords, NSTACKWORDS -.linkset FUNCTION_NAME.maxchanends, 0 -.linkset FUNCTION_NAME.maxtimers, 0 -.linkset FUNCTION_NAME.maxthreads, 0 - -.globl FUNCTION_NAME -.type FUNCTION_NAME, @function -.text -.cc_top FUNCTION_NAME.func, FUNCTION_NAME - -#define P_RXD r0 -#define P_RXA r1 -#define C_PKT r2 -#define BUFF r3 - -#define HEADER r4 -#define S0 r5 -#define S1 r6 -#define BUFF_HEAD r7 - - -/**************************************************** - **************************************************** - - extern struct { - uint32_t RxD; - uint32_t RxA; - } mipi_context; - - Non-zero return signals error. - - unsigned MipiPacketRx(uint8_t packet_buffer[]); - - void MipiPacketRx2( - const unsigned RxD, - const unsigned RxA, - streaming chanend c_pkt, - streaming chanend c_ctrl) - - **************************************************** - **************************************************** -*/ -.align 4 -.skip 0 -FUNCTION_NAME: - dualentsp NSTACKWORDS - std r4, r5, sp[1] - std r6, r7, sp[2] - std r8, r9, sp[3] - - -// Enable MIPI shim hardware -{ ldc r4, 0x01 ; ldc r9, 8 } -{ shl r4, r4, r9 ; } - ldc r9, XS1_PS_XCORE_CTRL0 - get r11, ps[r9] -{ or r4, r4, r11 ; } - set ps[r9], r4 - - -// Enable RxA event - setc res[P_RXA], XS1_SETC_COND_EQ // <-- event on edge of RxA - setc res[P_RXA], XS1_SETC_IE_MODE_EVENT // <-- do event, not interrupt - ldc r11, 0 - setd res[P_RXA], r11 // <-- falling edge - ldap r11, .L_MIPI_RXA_LOW - setv res[P_RXA], r11 - -// Enable events for the thread - setsr 1 // Sets the EEBLE bit (I couldn't find a #define!) - -// Set up interrupt for c_ctrl -// Note: has to be interrupt because we can't have events always enabled. -// Wait, no, events can be enabled/disabled per resource. I should be able to -// use an event... -/* -//////// TODO /////// -*/ - - -#if 0 // astew: I'm not sure if we need this, since we just turned on the shim - // but I don't see why we couldn't start receiving a packet while - // setting up the RxA event or whatever. - - -// Check whether the RxA signal is currently high. -// If so, we need to wait until it is low or we will start reading mid-packet - -.L_WAIT_LOOP: -{ peek S0, res[P_RXA] ; } -{ ecallt S0 ; } -{ ; bt S0, .L_WAIT_LOOP } - -#endif - -.L_GET_NEXT_BUFFER: - -{ in BUFF, res[C_PKT] ; } -{ mov BUFF_HEAD, BUFF ; bf BUFF, .L_EXIT_THREAD } // <-- exit if null buffer - -.L_WAIT_FOR_NEXT_PACKET: - -// peek S0, res[P_RXA] -// ecallt S0 - -// Receive packet header from data port -{ in HEADER, res[P_RXD] ; ldc S1, 0x00000030 } -{ and S1, S1, HEADER ; stw HEADER, BUFF[0] } - -// Check whether the header indicates it is a long or short packet. -// If long, enable the falling-edge event on RxA. -// If short, we already have the whole packet -{ eet S1, res[P_RXA] ; bt S1, .L_RX_LONG } - - -.L_RX_SHORT: -// A short packet was received. -{ out res[C_PKT], BUFF_HEAD ; bu .L_GET_NEXT_BUFFER } - - -.L_RX_LONG: -// Long packet incoming. The header has been placed into the buffer, but hold on -// to it until we get the rest of the packet. - -{ in S0, res[P_RXD] ; add BUFF, BUFF, 4 } -.L_RX_LONG_LOOP: - // { byterev S0, S0 ; } - { add BUFF, BUFF, 4 ; stw S0, BUFF[0] } - { in S0, res[P_RXD] ; bu .L_RX_LONG_LOOP } - - /**** - * RxA event vector target. - * - * RxA is a signal coming from the xcore.ai hardware's MIPI shim layer, - * signaling high while a packet is being received. On the falling edge of the - * signal, the packet has been fully received, and this event fires, breaking - * the thread out of the receive loop. - * - * If the received packet was not a multiple of 4 bytes, there will still be - * data left in the shift register. This event grabs any remaining data and puts - * it at the end of the received frame. - * - * When the data has been grabbed, it jumps down to .L_RxLONG_DataEnd - * - * This is only used for LONG frames. Short frames are always exactly 32 bits. - ***** - */ -.align 32 -.skip 20 -.L_MIPI_RXA_LOW: - -// Clear event on RxA port -{ in r11, res[P_RXA] ; } // <-- aligned 20 mod 32 -// End input on P_RXD, so we can pull out any remaining bytes without blocking. -{ endin r11, res[P_RXD] ; } // <-- aligned 24 mod 32 - -// NOTE: r11 will contain a number of BITS remaining, instead of bytes. The -// .align 32's below just avoid the need to change it to dual-issue instructions -// for the BRU instruction. r11 will be one of {0,8,16,24} -{ in S1, res[P_RXD] ; bru r11 } // <-- aligned 28 mod 32 - - -// In each of the 4 cases, the very last byte of the transmission should end -// up in S2. The last byte contains error bits and whatnot - -// In the 0 case, the byte with the error details is already in S2, but in the -// most significant bits, so pull it down. -//.... is it already in S2..? If r11 is zero, what did the IN instruction paired -// with BRU do? I suspect this is wrong.. -// -.L_RXA_OUT_TAIL0: // (0 byte tail) -{ shr S0, S0, 24 ; } // <-- aligned 0 mod 32 - bu .L_RXA_DATA_END - -.align 32 -.L_RXA_OUT_TAIL1: // (1 Tail Byte; the status byte) -{ shr S0, S1, 24 ; } -{ ; stw S0, BUFF[0] } -{ ; bu .L_RXA_DATA_END } - -.align 32 -.L_RXA_OUT_TAIL2: // (2 Tail Bytes) -{ shr S1, S1, 16 ; } -{ shr S0, S1, 8 ; stw S1, BUFF[0] } -{ ; bu .L_RXA_DATA_END } - -.align 32 -.L_RXA_OUT_TAIL3: // (3 Tail Bytes) -{ shr S1, S1, 8 ; } -{ shr S0, S1, 16 ; stw S1, BUFF[0] } -{ ; bu .L_RXA_DATA_END } - -.align 8 -.L_RXA_DATA_END: - -// We've received the full packet and S0 contains the status byte - -// Disable the RxA event -// (any bits set in S2 other than LSb indicate an error) -{ edu res[P_RXA] ; shr S0, S0, 1 } - -// Here's how we'll deal with received packet errors for now: If there is a -// packet error (if S0 is nonzero right here) in this long packet, change the -// data length in the packet header to 0. The header is already in the buffer, -// so if the next layer really needs to know what we saw for a packet length, -// it can just look in the buffer. -// -// Actually, for now, just assert on any errors. -{ out res[C_PKT], BUFF_HEAD ; ecallt S0 } - -// And now we need a new buffer -// Also reenable events. -{ setsr 1 ; bu .L_GET_NEXT_BUFFER } - - -.L_EXIT_THREAD: - - ldd r8, r9, sp[3] - ldd r6, r7, sp[2] - ldd r4, r5, sp[1] - retsp NSTACKWORDS - -.size FUNCTION_NAME, .-FUNCTION_NAME -.cc_bottom FUNCTION_NAME.func - - - + +#include +#include + +// XOR VERSION +// BYTEREV DISABLED + +/**************************************************** + **************************************************** + +Then to receive a packet: + +- Load context data from global memory (RxA, RxD) +- If RxA is high, wait for it to go low. (so we don't start mid-packet) + - Initial version can just do this with polling so we don't need to do any + weird jiggery pokery with the RxA low event vector (which we're already + using for a different purpose). +- Once RxA is low, use IN to wait for the first word of the packet +- Put the first word in the buffer +- If it's a short packet, return + - Maybe it's a good idea to return the data type? +- Otherwse, do the data loop +- Return + - Maybe it's a good idea to return the data type? + + **************************************************** + **************************************************** + */ + +/* + See MIPI CSI section 9.1.2 + + typedef struct { + uint8_t data_id; + uint16_t word_count; // In MIPI a word is a byte + uint8_t vcx_ecc; + } mipi_packet_header_t; + + Note: When that 32-bit header is read in on the buffered port, the data_id + will be the *least* significant 8 bits because of the direction of this port's + shift register. + + Bits: + data_id[7:6] - least significant two bits of 4-bit Virtual Channel ID + data_id[5:0] - data_type + vcx_ecc[7:6] - most significant two bits of 4-bit Virtual Channel ID + vcx_ecc[5:0] - error correction code +*/ + +.issue_mode dual + +#define FUNCTION_NAME MipiPacketRx +#define NSTACKWORDS 8 + +.globl FUNCTION_NAME.nstackwords +.globl FUNCTION_NAME.maxthreads +.globl FUNCTION_NAME.maxtimers +.globl FUNCTION_NAME.maxchanends + +.linkset FUNCTION_NAME.nstackwords, NSTACKWORDS +.linkset FUNCTION_NAME.maxchanends, 0 +.linkset FUNCTION_NAME.maxtimers, 0 +.linkset FUNCTION_NAME.maxthreads, 0 + +.globl FUNCTION_NAME +.type FUNCTION_NAME, @function +.text +.cc_top FUNCTION_NAME.func, FUNCTION_NAME + +#define P_RXD r0 +#define P_RXA r1 +#define C_PKT r2 +#define BUFF r3 + +#define HEADER r4 +#define S0 r5 +#define S1 r6 +#define BUFF_HEAD r7 + + +/**************************************************** + **************************************************** + + extern struct { + uint32_t RxD; + uint32_t RxA; + } mipi_context; + + Non-zero return signals error. + + unsigned MipiPacketRx(uint8_t packet_buffer[]); + + void MipiPacketRx2( + const unsigned RxD, + const unsigned RxA, + streaming chanend c_pkt, + streaming chanend c_ctrl) + + **************************************************** + **************************************************** +*/ +.align 4 +.skip 0 +FUNCTION_NAME: + dualentsp NSTACKWORDS + std r4, r5, sp[1] + std r6, r7, sp[2] + std r8, r9, sp[3] + + +// Enable MIPI shim hardware +{ ldc r4, 0x01 ; ldc r9, 8 } +{ shl r4, r4, r9 ; } + ldc r9, XS1_PS_XCORE_CTRL0 + get r11, ps[r9] +{ or r4, r4, r11 ; } + set ps[r9], r4 + + +// Enable RxA event + setc res[P_RXA], XS1_SETC_COND_EQ // <-- event on edge of RxA + setc res[P_RXA], XS1_SETC_IE_MODE_EVENT // <-- do event, not interrupt + ldc r11, 0 + setd res[P_RXA], r11 // <-- falling edge + ldap r11, .L_MIPI_RXA_LOW + setv res[P_RXA], r11 + +// Enable events for the thread + setsr 1 // Sets the EEBLE bit (I couldn't find a #define!) + +// Set up interrupt for c_ctrl +// Note: has to be interrupt because we can't have events always enabled. +// Wait, no, events can be enabled/disabled per resource. I should be able to +// use an event... +/* +//////// TODO /////// +*/ + + +#if 0 // astew: I'm not sure if we need this, since we just turned on the shim + // but I don't see why we couldn't start receiving a packet while + // setting up the RxA event or whatever. + + +// Check whether the RxA signal is currently high. +// If so, we need to wait until it is low or we will start reading mid-packet + +.L_WAIT_LOOP: +{ peek S0, res[P_RXA] ; } +{ ecallt S0 ; } +{ ; bt S0, .L_WAIT_LOOP } + +#endif + +ldc r8, 0x80 +shl r9, r8, 8 +or r8, r8, r9 +shl r9, r8, 16 +or r8, r8, r9 + + +.L_GET_NEXT_BUFFER: + +{ in BUFF, res[C_PKT] ; } +{ mov BUFF_HEAD, BUFF ; bf BUFF, .L_EXIT_THREAD } // <-- exit if null buffer + +.L_WAIT_FOR_NEXT_PACKET: + +// peek S0, res[P_RXA] +// ecallt S0 + +// Receive packet header from data port +{ in HEADER, res[P_RXD] ; ldc S1, 0x00000030 } +{ and S1, S1, HEADER ; stw HEADER, BUFF[0] } + +// Check whether the header indicates it is a long or short packet. +// If long, enable the falling-edge event on RxA. +// If short, we already have the whole packet +{ eet S1, res[P_RXA] ; bt S1, .L_RX_LONG } + + +.L_RX_SHORT: +// A short packet was received. +{ out res[C_PKT], BUFF_HEAD ; bu .L_GET_NEXT_BUFFER } + + +.L_RX_LONG: +// Long packet incoming. The header has been placed into the buffer, but hold on +// to it until we get the rest of the packet. + +{ in S0, res[P_RXD] ; add BUFF, BUFF, 4 } +.L_RX_LONG_LOOP: + // { byterev S0, S0 ; } + { xor S0, S0, r8 ; } + { add BUFF, BUFF, 4 ; stw S0, BUFF[0] } + { in S0, res[P_RXD] ; bu .L_RX_LONG_LOOP } + + /**** + * RxA event vector target. + * + * RxA is a signal coming from the xcore.ai hardware's MIPI shim layer, + * signaling high while a packet is being received. On the falling edge of the + * signal, the packet has been fully received, and this event fires, breaking + * the thread out of the receive loop. + * + * If the received packet was not a multiple of 4 bytes, there will still be + * data left in the shift register. This event grabs any remaining data and puts + * it at the end of the received frame. + * + * When the data has been grabbed, it jumps down to .L_RxLONG_DataEnd + * + * This is only used for LONG frames. Short frames are always exactly 32 bits. + ***** + */ +.align 32 +.skip 20 +.L_MIPI_RXA_LOW: + +// Clear event on RxA port +{ in r11, res[P_RXA] ; } // <-- aligned 20 mod 32 +// End input on P_RXD, so we can pull out any remaining bytes without blocking. +{ endin r11, res[P_RXD] ; } // <-- aligned 24 mod 32 + +// NOTE: r11 will contain a number of BITS remaining, instead of bytes. The +// .align 32's below just avoid the need to change it to dual-issue instructions +// for the BRU instruction. r11 will be one of {0,8,16,24} +{ in S1, res[P_RXD] ; bru r11 } // <-- aligned 28 mod 32 + + +// In each of the 4 cases, the very last byte of the transmission should end +// up in S2. The last byte contains error bits and whatnot + +// In the 0 case, the byte with the error details is already in S2, but in the +// most significant bits, so pull it down. +//.... is it already in S2..? If r11 is zero, what did the IN instruction paired +// with BRU do? I suspect this is wrong.. +// +.L_RXA_OUT_TAIL0: // (0 byte tail) +{ shr S0, S0, 24 ; } // <-- aligned 0 mod 32 + bu .L_RXA_DATA_END + +.align 32 +.L_RXA_OUT_TAIL1: // (1 Tail Byte; the status byte) +{ shr S0, S1, 24 ; } +{ ; stw S0, BUFF[0] } +{ ; bu .L_RXA_DATA_END } + +.align 32 +.L_RXA_OUT_TAIL2: // (2 Tail Bytes) +{ shr S1, S1, 16 ; } +{ shr S0, S1, 8 ; stw S1, BUFF[0] } +{ ; bu .L_RXA_DATA_END } + +.align 32 +.L_RXA_OUT_TAIL3: // (3 Tail Bytes) +{ shr S1, S1, 8 ; } +{ shr S0, S1, 16 ; stw S1, BUFF[0] } +{ ; bu .L_RXA_DATA_END } + +.align 8 +.L_RXA_DATA_END: + +// We've received the full packet and S0 contains the status byte + +// Disable the RxA event +// (any bits set in S2 other than LSb indicate an error) +{ edu res[P_RXA] ; shr S0, S0, 1 } + +// Here's how we'll deal with received packet errors for now: If there is a +// packet error (if S0 is nonzero right here) in this long packet, change the +// data length in the packet header to 0. The header is already in the buffer, +// so if the next layer really needs to know what we saw for a packet length, +// it can just look in the buffer. +// +// Actually, for now, just assert on any errors. +{ out res[C_PKT], BUFF_HEAD ; } + +// And now we need a new buffer +// Also reenable events. +{ setsr 1 ; bu .L_GET_NEXT_BUFFER } + + +.L_EXIT_THREAD: + + ldd r8, r9, sp[3] + ldd r6, r7, sp[2] + ldd r4, r5, sp[1] + retsp NSTACKWORDS + +.size FUNCTION_NAME, .-FUNCTION_NAME +.cc_bottom FUNCTION_NAME.func + + + diff --git a/python/decode_downsampled.py b/python/decode_downsampled.py new file mode 100644 index 00000000..ba82dbf0 --- /dev/null +++ b/python/decode_downsampled.py @@ -0,0 +1,42 @@ +""" +Info of RAW streams + +SBGGR8: + 640x480 : pixels + BGGR is the order of the Bayer pattern + +SBGGR10_CSI2P : + 640x480 stride 800 : bytes per line + SBGGR10_CSI2P : 10bits per pixel, CSI2 packed format + BGGR is the order of the Bayer pattern + few padding bytes on the end of every row to match bits +""" + +import cv2 +import matplotlib.pyplot as plt +import numpy as np +from PIL import Image # just to avoid color BGR issues when writting +from dotenv import load_dotenv +load_dotenv() # take environment variables from .env. + +from utils import * + +input_name = os.getenv('BINARY_IMG_PATH') + +# read the data +with open(input_name, "rb") as f: + data = f.read() + +# unpack +width, height = 160, 120 +buffer = np.frombuffer(data, dtype=np.uint8) +img = buffer.reshape(height, width, 3) +print("unpacked_data") + +# show image +plt.figure() +plt.imshow(img) +plt.show() + +# show histograms +show_histogram_by_channel(img) \ No newline at end of file diff --git a/python/decode_raw8.py b/python/decode_raw8.py index 31ee0f10..401b64e0 100644 --- a/python/decode_raw8.py +++ b/python/decode_raw8.py @@ -47,7 +47,9 @@ # unpack -buffer = np.frombuffer(data, dtype=np.uint8) +buffer = np.frombuffer(data, dtype=np.int8) + 128 # convert to uint8 +buffer = buffer.astype(np.uint8) + img = buffer.reshape(height, width, 1) print("unpacked_data") diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index e024875f..c89a4aee 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -96,13 +96,14 @@ #define camera_init(x) imx219_init(x) #define camera_start(x) imx219_stream_start(x) #define camera_configure(x) imx219_configure_mode(x) +#define camera_set_exposure(iic,ex) imx219_set_gain_dB(iic,ex) // Camera dependant (do not edit) #define MIPI_LINE_WIDTH_BYTES MIPI_IMAGE_WIDTH_BYTES #define MIPI_MAX_PKT_SIZE_BYTES ((MIPI_LINE_WIDTH_BYTES) + 4) #define MIPI_TILE 1 #define EXPECTED_FORMAT CONFIG_MIPI_FORMAT //backward compatibility - +#define MIPI_EXPECTED_FORMAT CONFIG_MIPI_FORMAT //backward compatibility // SRAM Image storage (do not edit) //TODO check maximum storage size for the image #define MAX_MEMORY_SIZE 500000 << 2 //becasue half needed is code @@ -112,4 +113,55 @@ #endif + + +// ---------------------------------------------------------------- +// ---------------------------------------------------------------- + +#define SENSOR_BIT_DEPTH (8) +#define SENSOR_RAW_IMAGE_WIDTH_PIXELS MIPI_IMAGE_WIDTH_PIXELS +#define SENSOR_RAW_IMAGE_HEIGHT_PIXELS MIPI_IMAGE_HEIGHT_PIXELS + +#define APP_DECIMATION_FACTOR (4) + +#define APP_IMAGE_WIDTH_PIXELS (SENSOR_RAW_IMAGE_WIDTH_PIXELS \ + / APP_DECIMATION_FACTOR) + +#define APP_IMAGE_HEIGHT_PIXELS (SENSOR_RAW_IMAGE_HEIGHT_PIXELS \ + / APP_DECIMATION_FACTOR) + +#define APP_IMAGE_CHANNEL_COUNT (3) + +#define APP_IMAGE_SIZE_PIXELS (APP_IMAGE_WIDTH_PIXELS \ + * APP_IMAGE_HEIGHT_PIXELS) + +#define APP_IMAGE_SIZE_BYTES (APP_IMAGE_SIZE_PIXELS \ + * APP_IMAGE_CHANNEL_COUNT ) + + +#define CHAN_RED 0 +#define CHAN_GREEN 1 +#define CHAN_BLUE 2 + +// Number of bits to collapse channel cardinality (larger value results in fewer +// histogram bins) +#ifndef APP_HISTOGRAM_QUANTIZATION_BITS +#define APP_HISTOGRAM_QUANTIZATION_BITS (2) +#endif + + +// Not every pixel of the image will be sampled. This is the distance between +// sampled values in a row. +#ifndef APP_HISTOGRAM_SAMPLE_STEP +#define APP_HISTOGRAM_SAMPLE_STEP (1) +#endif + +// The percentile to look for when applying white balance adjustments, as a +// fraction. (0.95 will find the value which 95% of pixels are less than or +// equal to) +#ifndef APP_WB_PERCENTILE +#define APP_WB_PERCENTILE (0.95) +#endif + + #endif // sensor_H \ No newline at end of file diff --git a/sensors/api/sensor_control.h b/sensors/api/sensor_control.h new file mode 100644 index 00000000..cbe44ac4 --- /dev/null +++ b/sensors/api/sensor_control.h @@ -0,0 +1,23 @@ + +#pragma once + +#include "xccompat.h" + + +#ifdef __XC__ + +typedef interface sensor_control_if { + void set_exposure(unsigned exposure); +} sensor_control_if; + +void sensor_control( + server interface sensor_control_if sc, + client interface i2c_master_if i2c); + +#endif + +/* These wrappers are for calling client interface functions from C */ +void sensor_control_set_exposure( + CLIENT_INTERFACE(sensor_control_if, sc), + const unsigned exposure + ); \ No newline at end of file diff --git a/sensors/imx219.h b/sensors/imx219.h index 3eb4e085..306b98d2 100644 --- a/sensors/imx219.h +++ b/sensors/imx219.h @@ -18,36 +18,26 @@ typedef struct #ifdef __XC__ // configure registers -#if (CONFIG_MODE == MODE_VGA_640x480) +#if ((CONFIG_MODE == 0) || (CONFIG_MODE == 1)) #define CONFIG_REG mode_640_480_regs - -#elif (CONFIG_MODE == MODE_UXGA_1640x1232) +#elif (CONFIG_MODE == 2) #define CONFIG_REG mode_1640_1232_regs - -#elif (CONFIG_MODE == MODE_WQSXGA_3280x2464) - #define CONFIG_REG mode_3280x2464_regs - -#elif (CONFIG_MODE == MODE_FHD_1920x1080) - #define CONFIG_REG mode_1920_1080_regs - #else - #error CONFIG_MODE_NOT_VALID + #error "Invalid configuration mode" #endif // Configure formats -#if CONFIG_MIPI_FORMAT == MIPI_DT_RAW10 +#if EXPECTED_FORMAT == MIPI_DT_RAW10 #define DATA_FORMAT_REGS raw10_framefmt_regs -#elif CONFIG_MIPI_FORMAT == MIPI_DT_RAW8 +#elif EXPECTED_FORMAT == MIPI_DT_RAW8 #define DATA_FORMAT_REGS raw8_framefmt_regs -#else - #error CONFIG_MIPI_NOT_VALID #endif // configure FPS #if defined(FPS_13) - #define PLL_VT_MPY 0x0017 + #define PLL_VT_MPY 0x0030 #elif defined(FPS_24) #define PLL_VT_MPY 0x0047 #elif defined(FPS_30) @@ -59,7 +49,7 @@ typedef struct #elif defined(FPS_76) #define PLL_VT_MPY 0x00E0 #else - #warning "fps not defined, selecting default value" + #warning fps not defined, selecting default value #define PLL_VT_MPY 0x0027 #endif @@ -72,10 +62,6 @@ int imx219_stream_stop(client interface i2c_master_if i2c); int imx219_set_gain_dB(client interface i2c_master_if i2c, uint32_t dBGain); int imx219_set_binning(client interface i2c_master_if i2c, uint32_t H_binning, uint32_t V_binning); int imx219_read(client interface i2c_master_if i2c, uint16_t addr); -int imx219_set_flip(client interface i2c_master_if i2c, uint8_t flip_mode); - -// read void imx219_read_gains(client interface i2c_master_if i2c, uint16_t values[5]); - #endif \ No newline at end of file diff --git a/sensors/imx219.xc b/sensors/imx219.xc index 22023df3..333a61c4 100644 --- a/sensors/imx219.xc +++ b/sensors/imx219.xc @@ -7,7 +7,7 @@ #include "imx219.h" #include "imx219_reg.h" - +#define GAIN_DB 40 static int i2c_write(client interface i2c_master_if i2c, int reg, int value) { @@ -37,7 +37,7 @@ static int i2c_write_table(client interface i2c_master_if i2c, } if (address & 0x8000) { address &= 0x7fff; - ret = i2c_write(i2c, address, value >> 8); // B1 B2 B3 B4 -> B1 B2 + ret = i2c_write(i2c, address, value >> 8); // B1 B2 B3 B4 -> B1 B2 ret |= i2c_write(i2c, address+1, value & 0xff); // B1 B2 B3 B4 -> B3 B4 } else { ret = i2c_write(i2c, address, value); @@ -72,8 +72,8 @@ static int i2c_write_table_val(client interface i2c_master_if i2c, address &= 0x7fff; } mode = 'c'; - printf("mode=%c , adress = 0x%04x, value = 0x%02x\n", mode, address, value >> 8); - printf("mode=%c , adress+ = 0x%04x, value = 0x%02x\n", mode, address+1, value & 0xff); + printf("mode=%c , address = 0x%04x, value = 0x%02x\n", mode, address, value >> 8); + printf("mode=%c , address+ = 0x%04x, value = 0x%02x\n", mode, address+1, value & 0xff); // continous writte ret = i2c_write(i2c, address, value >> 8); // B1 B2 B3 B4 -> B1 B2 ret |= i2c_write(i2c, address+1, value & 0xff); // B1 B2 B3 B4 -> B3 B4 @@ -83,7 +83,7 @@ static int i2c_write_table_val(client interface i2c_master_if i2c, // single writte mode = 's'; ret = i2c_write(i2c, address, value); - printf("mode=%c , adress = 0x%04x, value = 0x%02x\n", mode, address, value); + printf("mode=%c , address = 0x%04x, value = 0x%02x\n", mode, address, value); } if (ret < 0) { return ret; @@ -133,10 +133,7 @@ int imx219_configure_mode(client interface i2c_master_if i2c) ret = i2c_write_table_val(i2c, DATA_FORMAT_REGS, sizeof(DATA_FORMAT_REGS) / sizeof(DATA_FORMAT_REGS[0])); // set binning ret = i2c_write_table_val(i2c, binning_regs, sizeof(binning_regs) / sizeof(binning_regs[0])); - // set flip - ret = imx219_set_flip(i2c, SELECTED_FLIP); return ret; - } @@ -189,10 +186,4 @@ int imx219_set_gain_dB(client interface i2c_master_if i2c, } -int imx219_set_flip(client interface i2c_master_if i2c, uint8_t flip_mode){ - int ret = 0; - ret |= i2c_write(i2c, ORIENTATION_REG, flip_mode); - return ret; -} - // https://github.com/torvalds/linux/blob/63355b9884b3d1677de6bd1517cd2b8a9bf53978/drivers/media/i2c/imx219.c#L993 diff --git a/sensors/imx219_reg.h b/sensors/imx219_reg.h index 0df18338..e2966cbe 100644 --- a/sensors/imx219_reg.h +++ b/sensors/imx219_reg.h @@ -6,47 +6,28 @@ #define TRSTUS 200 // CSI LANE -#define CSI_LANE_MODE_REG 0x0114 +#define CSI_LANE_MODE_REG 0x0114 #define CSI_LANE_MODE_2_LANES 1 #define CSI_LANE_MODE_4_LANES 3 // BINNING -#define BINNING_MODE_REG 0x0174 // HORIZONTAL | VERTICAL -#define BINNING_NONE 0x0000 -#define BINNING_2X2 0x0101 -#define BINNING_4X4 0x0202 -#define BINNING_2Sx2 0x0302 -#define BINNING_2X2_AN 0x0303 - -#if ((CONFIG_MODE == MODE_1920_1080) || (CONFIG_MODE == MODE_WQSXGA_RAW8)) - #define BINNING_MODE BINNING_NONE - #define VTPXCK_DIV 0x05 -#else - #define BINNING_MODE BINNING_2X2 - #define VTPXCK_DIV 0x0A -#endif +#define BINNING_MODE_REG 0x0174 +#define BINNING_NONE 0x0000 +#define BINNING_2X2 0x0101 + +#define BINNING_MODE BINNING_2X2 // PLL settings #define PREPLLCK_VT_DIV_REG 0x0304 +#define PREPLLCK_OP_DIV 0x0305 +#define PREDVIDE_2 0x02 #define PLL_VT_MPY_REG 0x0306 +#define PLL_OP_MPY 0x0010 // no effect in timing performance // Gain params -#define GAIN_MIN_DB 0 -#define GAIN_MAX_DB 84 -#define GAIN_DEFAULT_DB 28 -#define GAIN_DB GAIN_DEFAULT_DB - -// Orientation -#define ORIENTATION_REG 0x0172 - // H | V -#define FLIP_NONE (0 | (0 << 1)) -#define FLIP_HORIZONTAL (1 | (0 << 1)) -#define FLIP_VERTICAL (0 | (1 << 1)) -#define FLIP_BOTH (1 | (1 << 1)) -#define SELECTED_FLIP FLIP_NONE - - - +#define GAIN_MIN_DB 0 +#define GAIN_MAX_DB 84 +#define GAIN_DEFAULT_DB 50 // --------- REG GROUP definitions ---------------------------------------------------- static imx219_settings_t imx219_common_regs[] = { @@ -65,19 +46,20 @@ static imx219_settings_t imx219_common_regs[] = { {0x30eb, 0x09}, /* PLL Clock Table */ - { 0x812A, 0x1800 }, // EXCK_FREQ 24.00, for 24 Mhz - { 0x0304, 0x02 }, // PREPLLCK_VT_DIV 2, for pre divide by 2 - { 0x0305, 0x02 }, // PREPLLCK_OP_DIV 2, for pre divide by 2 - { 0x8306, PLL_VT_MPY}, // PLL_VT_MPY 0x27, for multiply by 39, pixclk=187.2 MHz - { 0x830C, 0x40}, // PLL_OP_MPY 0x40, for multiply by 64, MIPI clk=768 MHz - { 0x0301, VTPXCK_DIV}, // VTPXCK_DIV 5, // Options: 05, 08 or 0A (higher the slowest) - { 0x0303, 0x01 }, // VTSYCK_DIV 1, ? - { 0x0309, 0x08 }, // OPPXCK_DIV 8, has to match RAW8 if you have raw8 - { 0x030B, 0x01 }, // OPSYCK_DIV 1, has to be 1? + { 0x812A, 0x1800 }, /* EXCK_FREQ 24.00, for 24 Mhz */ + { 0x0304, 0x02 }, /* PREPLLCK_VT_DIV 2, for pre divide by 2 */ + { 0x0305, 0x02 }, /* PREPLLCK_OP_DIV 2, for pre divide by 2 */ + { 0x8306, PLL_VT_MPY}, /* PLL_VT_MPY 0x27, for multiply by 39, pixclk=187.2 MHz */ + { 0x830C, PLL_OP_MPY}, /* PLL_OP_MPY 0x40, for multiply by 64, MIPI clk=768 MHz */ + { 0x0301, 0x0A }, /* VTPXCK_DIV 5, ? */ + { 0x0303, 0x01 }, /* VTSYCK_DIV 1, ? */ + { 0x0309, 0x0A }, /* OPPXCK_DIV 8, has to match RAW8 if you have raw8*/ + { 0x030B, 0x01 }, /* OPSYCK_DIV 1, has to be 1? */ - // min_line_blanking_pck + // pck clock {0x1148, 0x00}, - {0x1149, 0xA8}, + {0x1149, 0xF0}, + /* Undocumented registers */ {0x455e, 0x00}, @@ -107,11 +89,14 @@ static imx219_settings_t imx219_common_regs[] = { }; + + static imx219_settings_t imx219_lanes_regs[] = { {CSI_LANE_MODE_REG, CSI_LANE_MODE_2_LANES} }; + static imx219_settings_t mode_640_480_regs[] = { {0x0164, 0x03}, {0x0165, 0xe8}, @@ -150,43 +135,6 @@ static imx219_settings_t mode_1640_1232_regs[] = { {0x0627, 0xd0}, }; -static imx219_settings_t mode_1920_1080_regs[] = { - {0x0164, 0x02}, - {0x0165, 0xa8}, - {0x0166, 0x0a}, - {0x0167, 0x27}, - {0x0168, 0x02}, - {0x0169, 0xb4}, - {0x016a, 0x06}, - {0x016b, 0xeb}, - {0x016c, 0x07}, - {0x016d, 0x80}, - {0x016e, 0x04}, - {0x016f, 0x38}, - {0x0624, 0x07}, - {0x0625, 0x80}, - {0x0626, 0x04}, - {0x0627, 0x38}, -}; - -static imx219_settings_t mode_3280x2464_regs[] = { - {0x0164, 0x00}, - {0x0165, 0x00}, - {0x0166, 0x0c}, - {0x0167, 0xcf}, - {0x0168, 0x00}, - {0x0169, 0x00}, - {0x016a, 0x09}, - {0x016b, 0x9f}, - {0x016c, 0x0c}, - {0x016d, 0xd0}, - {0x016e, 0x09}, - {0x016f, 0xa0}, - {0x0624, 0x0c}, - {0x0625, 0xd0}, - {0x0626, 0x09}, - {0x0627, 0xa0}, -}; static imx219_settings_t raw10_framefmt_regs[] = { {0x018c, 0x0a}, diff --git a/sensors/sensor_control.xc b/sensors/sensor_control.xc new file mode 100644 index 00000000..26471f59 --- /dev/null +++ b/sensors/sensor_control.xc @@ -0,0 +1,28 @@ +#include +#include +#include + +#include "i2c.h" +#include "sensor_control.h" +#include "sensor.h" + +void sensor_control( + server interface sensor_control_if sc, + client interface i2c_master_if i2c) +{ + while(1){ + select { + case sc.set_exposure(unsigned exposure): + camera_set_exposure(i2c, exposure); + break; + } + } +} + + +void sensor_control_set_exposure( + client interface sensor_control_if sc, + unsigned exposure) +{ + sc.set_exposure(exposure); +} \ No newline at end of file From 53fc2f26bda8d5fa52c16f8e29bf9ae90f67e7fe Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Mon, 5 Jun 2023 10:12:12 +0100 Subject: [PATCH 030/306] adding raw handler --- camera/api/utils.h | 2 + camera/src/packet_handler.c | 83 ++++++- camera/src/user_api.c | 10 + camera/src/utils.c | 24 +- examples/take_picture_raw/CMakeLists.txt | 2 + examples/take_picture_raw/src/main.xc | 66 +++-- examples/take_picture_raw/src/mipi_main.h | 36 --- examples/take_picture_raw/src/mipi_main.xc | 231 ------------------ examples/take_picture_raw/src/process_frame.c | 43 ---- examples/take_picture_raw/src/process_frame.h | 19 -- 10 files changed, 160 insertions(+), 356 deletions(-) delete mode 100644 examples/take_picture_raw/src/mipi_main.h delete mode 100644 examples/take_picture_raw/src/mipi_main.xc delete mode 100644 examples/take_picture_raw/src/process_frame.c delete mode 100644 examples/take_picture_raw/src/process_frame.h diff --git a/camera/api/utils.h b/camera/api/utils.h index b0b3543c..88c55898 100644 --- a/camera/api/utils.h +++ b/camera/api/utils.h @@ -23,6 +23,8 @@ void write_image( uint8_t image[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); void c_memcpy(void *dst, void *src, size_t size); void writeBMP(const char *filename, uint8_t img[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); +void write_image_raw(const char* filename,uint8_t *image); + #if defined(__XC__) || defined(__cplusplus) } diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index dcf29f20..6d63e157 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -12,6 +12,13 @@ #include "utils.h" #include "sensor.h" + +#define RAW_CAPTURE 1 + +#if (RAW_CAPTURE) + uint8_t image_raw[MIPI_IMAGE_HEIGHT_PIXELS*MIPI_LINE_WIDTH_BYTES]; +#endif + /** * State needed for the vertical filter */ @@ -48,7 +55,6 @@ void handle_frame_start( } - static void handle_unknown_packet( const mipi_packet_t* pkt) @@ -56,7 +62,7 @@ void handle_unknown_packet( // Do nothing } -#define BYPASS_HFILTER 0 + /** * Handle a row of pixel data. * @@ -171,6 +177,18 @@ void handle_no_expected_lines(){ } } + +void handle_expected_format_raw(const mipi_packet_t* pkt){ + uint32_t pos = (ph_state.in_line_number) * MIPI_LINE_WIDTH_BYTES; + memcpy(&image_raw[pos], &pkt->payload[0], MIPI_LINE_WIDTH_BYTES); +} + + +void handle_frame_end_raw(){ + write_image_raw("capture_raw.bin", image_raw); + exit(0); +} + /** * Process a single packet. * @@ -247,6 +265,61 @@ void handle_packet( } +static +void handle_packet_raw( + const mipi_packet_t* pkt, + streaming_chanend_t c_out_row) +{ + + __attribute__((aligned(8))) + static int8_t output_buff[2][APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_WIDTH_PIXELS]; + static unsigned out_dex = 0; + + + // definitions + const mipi_header_t header = pkt->header; + const mipi_data_type_t data_type = MIPI_GET_DATA_TYPE(header); + + // At start-up we usually want to wait for a new frame before processing + // anything + if(ph_state.wait_for_frame_start + && data_type != MIPI_DT_FRAME_START) return; + + /* + The idea here is that logic that keeps the packet handler in a coherent + state, like tracking frame and line numbers, should go directly in here, but + logic that actually interprets, processes or reacts to packet data should go + into the individual functions. + */ + switch(data_type) + { + case MIPI_DT_FRAME_START: + ph_state.wait_for_frame_start = 0; + ph_state.in_line_number = 0; + ph_state.out_line_number = 0; + ph_state.frame_number++; + break; + + case MIPI_DT_FRAME_END: + handle_frame_end_raw(); + out_dex = 1 - out_dex; + break; + + case MIPI_EXPECTED_FORMAT: + handle_no_expected_lines(); + handle_expected_format_raw(pkt); + + ph_state.in_line_number++; + break; + + default: + // We've received a packet we don't know how to interpret. + handle_unknown_packet(pkt); + break; + } +} + + /** * Top level of the packet handling thread. Receives MIPI packets from the * packet receiver and passes them to `handle_packet()` for parsing and @@ -284,7 +357,11 @@ void mipi_packet_handler( //const mipi_data_type_t data_type = MIPI_GET_DATA_TYPE(header); // unsigned time_start = measure_time(); - handle_packet(pkt, c_out_row); + #if (RAW_CAPTURE) + handle_packet_raw(pkt, c_out_row); + #else + handle_packet(pkt, c_out_row); + #endif // unsigned time_proc = measure_time() - time_start; } diff --git a/camera/src/user_api.c b/camera/src/user_api.c index c006bf15..f9023bcd 100644 --- a/camera/src/user_api.c +++ b/camera/src/user_api.c @@ -60,6 +60,16 @@ unsigned camera_capture_image( return s_chan_in_word(c_cam_api); } +unsigned camera_capture_image_raw( + int8_t image_buff[CH][H][W], + streaming_chanend_t c_cam_api) +{ + int8_t *p_image = &image_buff[0][0][0]; + + s_chan_out_word(c_cam_api, (unsigned)p_image); + return s_chan_in_word(c_cam_api); +} + void camera_api_request_complete() { if(user_image){ diff --git a/camera/src/utils.c b/camera/src/utils.c index 1b9bc6ea..2597a358 100644 --- a/camera/src/utils.c +++ b/camera/src/utils.c @@ -112,9 +112,9 @@ void writeBMP(const char* filename, uint8_t img[APP_IMAGE_CHANNEL_COUNT][APP_IMA for (i = height - 1; i >= 0; i--) { for (j = 0; j < width; j++) { // Write the pixel data (assuming RGB order) - fwrite(&img[0][i][j], sizeof(unsigned char), 1, file); // Blue + fwrite(&img[2][i][j], sizeof(unsigned char), 1, file); // Blue fwrite(&img[1][i][j], sizeof(unsigned char), 1, file); // Green - fwrite(&img[2][i][j], sizeof(unsigned char), 1, file); // Red + fwrite(&img[0][i][j], sizeof(unsigned char), 1, file); // Red // For 4-channel images, you can write the alpha channel here // fwrite(&img[index + 3], sizeof(unsigned char), 1, file); // Alpha } @@ -131,3 +131,23 @@ void writeBMP(const char* filename, uint8_t img[APP_IMAGE_CHANNEL_COUNT][APP_IMA printf("image size (%dx%d)\n", APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_HEIGHT_PIXELS); free(img); } + + +// Write image to disk. This is called by camera main () to do the work +void write_image_raw( + const char* filename, + uint8_t *image) +{ + static FILE* img_file = NULL; + img_file = fopen(filename, "wb"); + for(uint16_t k = 0; k < MIPI_IMAGE_HEIGHT_PIXELS; k++){ + for(uint16_t j = 0; j < MIPI_LINE_WIDTH_BYTES; j++){ + uint32_t pos = k * MIPI_LINE_WIDTH_BYTES + j; + fwrite(&image[pos], sizeof(uint8_t), 1, img_file); + } + } + fclose(img_file); + printf("Outfile %s\n", filename); + printf("image size (%dx%d)\n", MIPI_LINE_WIDTH_BYTES, MIPI_IMAGE_HEIGHT_PIXELS); + free(image); +} \ No newline at end of file diff --git a/examples/take_picture_raw/CMakeLists.txt b/examples/take_picture_raw/CMakeLists.txt index e19ab2e1..d8e97883 100644 --- a/examples/take_picture_raw/CMakeLists.txt +++ b/examples/take_picture_raw/CMakeLists.txt @@ -50,9 +50,11 @@ set(APP_COMMON_LINK_LIBRARIES mipi::lib_mipi i2c::lib_i2c sensors::lib_imx + camera::lib_camera ) + #********************** # Tile Targets #********************** diff --git a/examples/take_picture_raw/src/main.xc b/examples/take_picture_raw/src/main.xc index 47a72322..7bc95048 100644 --- a/examples/take_picture_raw/src/main.xc +++ b/examples/take_picture_raw/src/main.xc @@ -1,22 +1,44 @@ -// Copyright (c) 2020, XMOS Ltd, All rights reserved -#include -#include -#include - -#include "i2c.h" -#include "mipi_main.h" - -// I2C interface ports -#define Kbps 400 -on tile[0]: port p_scl = XS1_PORT_1N; -on tile[0]: port p_sda = XS1_PORT_1O; - -int main(void) -{ - i2c_master_if i2c[1]; - par { - on tile[0]: i2c_master(i2c, 1, p_scl, p_sda, Kbps); - on tile[MIPI_TILE]: mipi_main(i2c[0]); - } - return 0; -} +// Copyright (c) 2020, XMOS Ltd, All rights reserved +#include +#include +#include + +#include "i2c.h" +#include "camera.h" + + +// I2C interface ports +#define Kbps 400 +on tile[0]: port p_scl = XS1_PORT_1N; +on tile[0]: port p_sda = XS1_PORT_1O; + + +/** +* Declaration of the MIPI interface ports: +* Clock, receiver active, receiver data valid, and receiver data +*/ +on tile[MIPI_TILE] : in port p_mipi_clk = XS1_PORT_1O; +on tile[MIPI_TILE] : in port p_mipi_rxa = XS1_PORT_1E; // activate +on tile[MIPI_TILE] : in port p_mipi_rxv = XS1_PORT_1I; // valid +on tile[MIPI_TILE] : buffered in port:32 p_mipi_rxd = XS1_PORT_8A; // data +on tile[MIPI_TILE] : clock clk_mipi = MIPI_CLKBLK; + + +int main(void) +{ + streaming chan c_cam_api; + i2c_master_if i2c[1]; + par { + on tile[0]: i2c_master(i2c, 1, p_scl, p_sda, Kbps); + + on tile[MIPI_TILE]: camera_main(tile[MIPI_TILE], + p_mipi_clk, + p_mipi_rxa, + p_mipi_rxv, + p_mipi_rxd, + clk_mipi, + i2c[0], + c_cam_api); + } + return 0; +} diff --git a/examples/take_picture_raw/src/mipi_main.h b/examples/take_picture_raw/src/mipi_main.h deleted file mode 100644 index 1ab51876..00000000 --- a/examples/take_picture_raw/src/mipi_main.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include -#include "mipi.h" - -#include "sensor.h" - -///TODO this is inside lib_mipi -#ifndef MIPI_CLKBLK -#define MIPI_CLKBLK XS1_CLKBLK_1 -#endif - -// Packets definitions -typedef struct -{ - mipi_header_t header; - uint8_t payload[MIPI_LINE_WIDTH_BYTES]; -} mipi_data_t; - -typedef struct -{ - mipi_header_t header; - uint8_t payload[MIPI_MAX_PKT_SIZE_BYTES]; -} mipi_packet_t; - -typedef struct -{ - unsigned frame_number; - unsigned line_number; -} image_rx_t; - -static mipi_packet_t packet_buffer[MIPI_PKT_BUFFER_COUNT]; - -#ifdef __XC__ -void mipi_main(client interface i2c_master_if i2c); -#endif diff --git a/examples/take_picture_raw/src/mipi_main.xc b/examples/take_picture_raw/src/mipi_main.xc deleted file mode 100644 index fac21aac..00000000 --- a/examples/take_picture_raw/src/mipi_main.xc +++ /dev/null @@ -1,231 +0,0 @@ -#include -#include -#include -#include // for ports -#include -#include // exit status -#include -#include -#include - -#include - -// I2C -#include "i2c.h" - -// MIPI -#include "mipi_main.h" - -// Sensor -#define MSG_SUCCESS "Stream start OK\n" -#define MSG_FAIL "Stream start Failed\n" - -// Image -#include "process_frame.h" - -// Globals -char end_transmission = 0; -mipi_data_type_t p_data_type = 0; - - -/** -* Declaration of the MIPI interface ports: -* Clock, receiver active, receiver data valid, and receiver data -*/ -on tile[MIPI_TILE] : in port p_mipi_clk = XS1_PORT_1O; -on tile[MIPI_TILE] : in port p_mipi_rxa = XS1_PORT_1E; // activate -on tile[MIPI_TILE] : in port p_mipi_rxv = XS1_PORT_1I; // valid -on tile[MIPI_TILE] : buffered in port:32 p_mipi_rxd = XS1_PORT_8A; // data -on tile[MIPI_TILE] : clock clk_mipi = MIPI_CLKBLK; - -/** - * The packet buffer is where the packet decoupler will tell the MIPI receiver - * thread to store received packets. - */ -#define DEMUX_DATATYPE 0 // RESERVED -#define DEMUX_MODE 0x00 // no demux -#define DEMUX_EN 0 // DISABLE DEMUX -#define MIPI_CLK_DIV 1 // CLK DIVIDER -#define MIPI_CFG_CLK_DIV 3 // CFG DIVIDER -#define REV(n) ((n << 24) | (((n>>16)<<24)>>16) | (((n<<16)>>24)<<16) | (n>>24)) - - -// Saves the image to a file. This is a bit tricky because we don't want to use an endless loop -void save_image_to_file(chanend flag) -{ - select - { - case flag :> int i: - { - // write to a file - write_image(); - delay_microseconds(200); // for stability //TODO maybe inside the function - exit(1); // end the program here - break; - } - } -} - -unsafe { -static -void handle_packet( - image_rx_t* img_rx, - const mipi_packet_t* unsafe pkt, - chanend flag) - { - // End here if just one transmission - if (end_transmission == 1){ - return; - } - - // definitions - const mipi_header_t header = pkt->header; - const mipi_data_type_t data_type = MIPI_GET_DATA_TYPE(header); - const unsigned is_long = MIPI_IS_LONG_PACKET(header); // not used for the moment - const unsigned word_count = MIPI_GET_WORD_COUNT(header); // not used for the moment - static uint8_t wait_for_clean_frame = 1; // static because it will change in the future - // printf("packet header = 0x%08x, wc=%d \n", REV(header), word_count); - - // We return until the start of frame is reached - if (wait_for_clean_frame == 1){ - if (data_type != MIPI_DT_FRAME_START){ - return; - } - else{ - wait_for_clean_frame = 0; - } - } - - // Use case by data type - switch (data_type) - { - case MIPI_DT_FRAME_START: { // Start of frame. Just reset line number. - img_rx->frame_number++; - img_rx->line_number = 0; - break; - } - - case EXPECTED_FORMAT:{ // save it in SRAM and increment line - // if line number is grater than expected, just reset the line number - if (img_rx->line_number >= MIPI_IMAGE_HEIGHT_PIXELS){ - break; // let pass the rest until next frame - } - // then copy - not_silly_memcpy( - &FINAL_IMAGE[img_rx->line_number][0], - &pkt->payload[0], - MIPI_LINE_WIDTH_BYTES); // here is data width - img_rx->line_number++; - break; - } - - case MIPI_DT_FRAME_END:{ // we signal that the frame is finish so we can write it to a file - if (end_transmission == 0){ //TODO not needed if and the end of transmission I just return - flag <: 1; - end_transmission = 1; - } - break; - } - - default:{ // error with frame type or protected types - break; - } - } - } - - -#pragma unsafe arrays -static -void mipi_packet_handler( - streaming chanend c_pkt, - streaming chanend c_ctrl, - chanend flag - ) -{ - image_rx_t img_rx = {0,0}; // stores the coordinates X, Y of the image - unsigned pkt_idx = 0; // packet index - - // Give the MIPI packet receiver a buffer - outuint((chanend) c_pkt, (unsigned) &packet_buffer[pkt_idx]); - pkt_idx = (pkt_idx + 1) & (MIPI_PKT_BUFFER_COUNT-1); - - while(1) { - // Wait for the receiver thread to tell us a new packet was completed. - mipi_packet_t * unsafe pkt = (mipi_packet_t*unsafe) inuint((chanend) c_pkt); - - // Give it a new buffer before processing the received one - outuint((chanend) c_pkt, (unsigned) &packet_buffer[pkt_idx]); - pkt_idx = (pkt_idx + 1) & (MIPI_PKT_BUFFER_COUNT-1); - - // Process the packet. We need to be finished with this and looped - // back up to grab the next MIPI packet BEFORE the receiver thread - // tries to give us the next packet. - handle_packet(&img_rx, pkt, flag); - - if (end_transmission == 1){ - return; - } - } -} -} - - -void mipi_main(client interface i2c_master_if i2c) -{ - printf("< Start of APP capture application >\n"); - streaming chan c_pkt; - streaming chan c_ctrl; - chan flag; - - - // See AN for MIPI shim - // 0x7E42 >> 0111 1110 0100 0010 - // in the explorer BOARD DPDN is swap - write_node_config_reg(tile[MIPI_TILE], - XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, - 0x7E42); //TODO decompose into different values - - // send packet to MIPI shim - MipiPacketRx_init(tile[MIPI_TILE], - p_mipi_rxd, - p_mipi_rxv, - p_mipi_rxa, - p_mipi_clk, - clk_mipi, - DEMUX_EN, - DEMUX_DATATYPE, - DEMUX_MODE, - MIPI_CLK_DIV, - MIPI_CFG_CLK_DIV); - - // Start camera and its configurations - int r = 0; - r |= camera_init(i2c); - delay_milliseconds(100); //TODO include this inside the function - r |= camera_configure(i2c); - delay_milliseconds(500); - - // Start streaming mode - r |= camera_start(i2c); - delay_milliseconds(2000); - - if (r != 0){ - printf(MSG_FAIL); - } - else{ - printf(MSG_SUCCESS); - } - - // ask the user to press the key "c" to capture the frame - // user_input(); //TODO not working with a par job - - - // start the different jobs (packet controller, handler, and post_process) - par - { - MipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); - mipi_packet_handler(c_pkt, c_ctrl, flag); - save_image_to_file(flag); - } -} - diff --git a/examples/take_picture_raw/src/process_frame.c b/examples/take_picture_raw/src/process_frame.c deleted file mode 100644 index 9e933afa..00000000 --- a/examples/take_picture_raw/src/process_frame.c +++ /dev/null @@ -1,43 +0,0 @@ - -#include "process_frame.h" -#include -#include -#include - -#define FINAL_IMAGE_FILENAME "img_raw.bin" -uint8_t FINAL_IMAGE[MIPI_IMAGE_HEIGHT_PIXELS][MIPI_LINE_WIDTH_BYTES]; - -// Write image to disk. This is called by camera main () to do the work -void write_image() -{ - static FILE* img_file = NULL; - img_file = fopen(FINAL_IMAGE_FILENAME, "wb"); - for(int k = 0; k < MIPI_IMAGE_HEIGHT_PIXELS; k++){ - for(int j = 0; j < MIPI_IMAGE_WIDTH_BYTES; j++){ - fwrite(&FINAL_IMAGE[k][j], sizeof(uint8_t), 1, img_file); - } - } - fclose(img_file); - printf("Outfile %s\n", FINAL_IMAGE_FILENAME); - printf("image size (%dx%d)\n", MIPI_LINE_WIDTH_BYTES, MIPI_IMAGE_HEIGHT_PIXELS); -} - - -// This is called when want to memcpy from Xc to C -void not_silly_memcpy( - void* dst, - void* src, - size_t size) -{ - memcpy(dst, src, size); -} - -/* -void user_input(void){ - printf("Enter the character 'c' to capture the frame : "); - int c = getchar(); - if(c != 99){ // == ascii "c" - exit(0); - } -} -*/ \ No newline at end of file diff --git a/examples/take_picture_raw/src/process_frame.h b/examples/take_picture_raw/src/process_frame.h deleted file mode 100644 index 635e6f2a..00000000 --- a/examples/take_picture_raw/src/process_frame.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include "mipi_main.h" - -#ifdef __XC__ -extern "C" { -#endif - -// Store the image -extern uint8_t FINAL_IMAGE[MIPI_IMAGE_HEIGHT_PIXELS][MIPI_LINE_WIDTH_BYTES]; - -void write_image(); -void not_silly_memcpy(void *dst, void *src, size_t size); -// void user_input(void); - -#ifdef __XC__ -} -#endif \ No newline at end of file From dfe5226ba7180d5ae80429815d6d91b7d45c31d1 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Mon, 5 Jun 2023 11:20:59 +0100 Subject: [PATCH 031/306] delegate raw to user api and new line at EoF --- camera/api/camera.h | 2 +- camera/api/image_vfilter.h | 2 +- camera/api/isp.h | 2 +- camera/api/packet_handler.h | 2 +- camera/api/statistics.h | 2 -- camera/api/user_api.h | 6 ++++++ camera/api/utils.h | 2 +- camera/src/image_hfilter.c | 3 --- camera/src/image_vfilter.c | 2 +- camera/src/isp.c | 2 +- camera/src/packet_handler.c | 33 +++++++++++++-------------------- camera/src/statistics.c | 2 +- camera/src/user_api.c | 31 ++++++++++++++++++++----------- camera/src/utils.c | 2 +- 14 files changed, 48 insertions(+), 45 deletions(-) diff --git a/camera/api/camera.h b/camera/api/camera.h index 177eb19f..ee122e9d 100644 --- a/camera/api/camera.h +++ b/camera/api/camera.h @@ -46,4 +46,4 @@ void camera_main( client interface i2c_master_if i2c, streaming chanend c_user_api); -#endif //__XC__ \ No newline at end of file +#endif //__XC__ diff --git a/camera/api/image_vfilter.h b/camera/api/image_vfilter.h index 31b04c68..afd8ee8d 100644 --- a/camera/api/image_vfilter.h +++ b/camera/api/image_vfilter.h @@ -78,4 +78,4 @@ unsigned image_vfilter_drain( } #endif -#endif //IMAGE_VFILTER_H \ No newline at end of file +#endif //IMAGE_VFILTER_H diff --git a/camera/api/isp.h b/camera/api/isp.h index a3227c27..5a76f526 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -26,4 +26,4 @@ typedef struct { void AWB_compute_gains(global_stats_t *gstats, AWB_gains_t *gains); void AWB_print_gains(AWB_gains_t *gains); -#endif \ No newline at end of file +#endif diff --git a/camera/api/packet_handler.h b/camera/api/packet_handler.h index b7daf2b7..d0a5e0b6 100644 --- a/camera/api/packet_handler.h +++ b/camera/api/packet_handler.h @@ -30,4 +30,4 @@ void mipi_packet_handler( } #endif -#endif // PACKET_HANDLER_H \ No newline at end of file +#endif // PACKET_HANDLER_H diff --git a/camera/api/statistics.h b/camera/api/statistics.h index 552f0d3f..bfc787db 100644 --- a/camera/api/statistics.h +++ b/camera/api/statistics.h @@ -60,5 +60,3 @@ void statistics_thread( #if defined(__XC__) || defined(__cplusplus) } #endif - - diff --git a/camera/api/user_api.h b/camera/api/user_api.h index 596bc12a..1ece61cc 100644 --- a/camera/api/user_api.h +++ b/camera/api/user_api.h @@ -6,6 +6,8 @@ #define H APP_IMAGE_HEIGHT_PIXELS #define W APP_IMAGE_WIDTH_PIXELS +#define RAW_CAPTURE 1 + // Image structure typedef struct { int8_t pix[CH][H][W]; @@ -23,3 +25,7 @@ void camera_api_request_complete(); void camera_api_request_update( const int8_t image_row[CH][W], const unsigned row_index); + +// RAW +void camera_api_request_update_raw(uint16_t line_number, uint8_t *img_row_ptr); +void camera_api_request_complete_raw(); diff --git a/camera/api/utils.h b/camera/api/utils.h index 88c55898..a3019724 100644 --- a/camera/api/utils.h +++ b/camera/api/utils.h @@ -28,4 +28,4 @@ void write_image_raw(const char* filename,uint8_t *image); #if defined(__XC__) || defined(__cplusplus) } -#endif \ No newline at end of file +#endif diff --git a/camera/src/image_hfilter.c b/camera/src/image_hfilter.c index 8430015e..f15fe0e8 100644 --- a/camera/src/image_hfilter.c +++ b/camera/src/image_hfilter.c @@ -128,6 +128,3 @@ void image_hfilter( HFILTER_INPUT_STRIDE, APP_IMAGE_WIDTH_PIXELS); } - - - diff --git a/camera/src/image_vfilter.c b/camera/src/image_vfilter.c index ab4e61e5..767f8a8e 100644 --- a/camera/src/image_vfilter.c +++ b/camera/src/image_vfilter.c @@ -181,4 +181,4 @@ unsigned image_vfilter_drain( } return 0; -} \ No newline at end of file +} diff --git a/camera/src/isp.c b/camera/src/isp.c index 6afdbf44..8ff091a9 100644 --- a/camera/src/isp.c +++ b/camera/src/isp.c @@ -68,4 +68,4 @@ void AWB_compute_gains(global_stats_t *gstats, AWB_gains_t *gains){ void AWB_print_gains(AWB_gains_t *gains){ printf("awb:%f,%f,%f\n",gains->alfa,gains->beta,gains->gamma); -} \ No newline at end of file +} diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index 6d63e157..d763161a 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -13,22 +13,13 @@ #include "sensor.h" -#define RAW_CAPTURE 1 -#if (RAW_CAPTURE) - uint8_t image_raw[MIPI_IMAGE_HEIGHT_PIXELS*MIPI_LINE_WIDTH_BYTES]; -#endif - -/** - * State needed for the vertical filter - */ +// State needed for the vertical filter static vfilter_acc_t vfilter_accs[APP_IMAGE_CHANNEL_COUNT][VFILTER_ACC_COUNT]; -/** - * Contains the local state info for the packet handler thread. - */ +// Contains the local state info for the packet handler thread. static struct { unsigned wait_for_frame_start; unsigned frame_number; @@ -59,10 +50,12 @@ static void handle_unknown_packet( const mipi_packet_t* pkt) { - // Do nothing + //TODO: manage uknown packets + // uknown packets could be the following: + // 1 - sensor specific packets (this could be useful for having more information about the frame) + // 2 - error packets (in this case mipi reciever will raise an exception, but in the future we want to handle them here) } - /** * Handle a row of pixel data. * @@ -177,16 +170,16 @@ void handle_no_expected_lines(){ } } - +// send the line number and the actual raw to the user api void handle_expected_format_raw(const mipi_packet_t* pkt){ - uint32_t pos = (ph_state.in_line_number) * MIPI_LINE_WIDTH_BYTES; - memcpy(&image_raw[pos], &pkt->payload[0], MIPI_LINE_WIDTH_BYTES); + camera_api_request_update_raw( + (uint16_t) ph_state.in_line_number, + (uint8_t *) &pkt->payload[0]); } - +// end of frame void handle_frame_end_raw(){ - write_image_raw("capture_raw.bin", image_raw); - exit(0); + camera_api_request_complete_raw(); } /** @@ -381,4 +374,4 @@ void mipi_packet_handler( // We don't need to watch for new rows from the vertical decimator here // because we know a priori that blue will be the last one out. -*/ \ No newline at end of file +*/ diff --git a/camera/src/statistics.c b/camera/src/statistics.c index ccf98e3e..5dc0e61b 100644 --- a/camera/src/statistics.c +++ b/camera/src/statistics.c @@ -188,4 +188,4 @@ sampled pixels (which is known a priori). Because every bin is adjusted by the same factor, we can just wait to apply the adjustment until we get here. histogram_norm_factor is just the inverse of the total number of sampled pixels. -*/ \ No newline at end of file +*/ diff --git a/camera/src/user_api.c b/camera/src/user_api.c index f9023bcd..fd9843d9 100644 --- a/camera/src/user_api.c +++ b/camera/src/user_api.c @@ -11,7 +11,12 @@ static image_t *user_image; streaming_chanend_t c_user_api; +#if (RAW_CAPTURE) + uint8_t image_raw[MIPI_IMAGE_HEIGHT_PIXELS*MIPI_LINE_WIDTH_BYTES]; +#endif + +// ---------------------------------------------------------------- void camera_api_init( streaming_chanend_t c_api) { @@ -60,20 +65,24 @@ unsigned camera_capture_image( return s_chan_in_word(c_cam_api); } -unsigned camera_capture_image_raw( - int8_t image_buff[CH][H][W], - streaming_chanend_t c_cam_api) -{ - int8_t *p_image = &image_buff[0][0][0]; - - s_chan_out_word(c_cam_api, (unsigned)p_image); - return s_chan_in_word(c_cam_api); -} - void camera_api_request_complete() { if(user_image){ s_chan_out_word(c_user_api, 1); user_image = NULL; } -} \ No newline at end of file +} + + +// ---------------------------------------------------------------- +void camera_api_request_update_raw(uint16_t line_number, uint8_t* img_row_ptr){ + uint32_t pos = (line_number) * MIPI_LINE_WIDTH_BYTES; + c_memcpy((void*) &image_raw[pos], + (void*) &img_row_ptr[0], + MIPI_LINE_WIDTH_BYTES); +} + +void camera_api_request_complete_raw(){ + write_image_raw("capture.bin", image_raw); + exit(0); +} diff --git a/camera/src/utils.c b/camera/src/utils.c index 2597a358..55cd5df7 100644 --- a/camera/src/utils.c +++ b/camera/src/utils.c @@ -150,4 +150,4 @@ void write_image_raw( printf("Outfile %s\n", filename); printf("image size (%dx%d)\n", MIPI_LINE_WIDTH_BYTES, MIPI_IMAGE_HEIGHT_PIXELS); free(image); -} \ No newline at end of file +} From bb180010f72a4a10c6ec924480e4145bc9b33089 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Mon, 5 Jun 2023 11:34:18 +0100 Subject: [PATCH 032/306] restoring cmake from camera --- camera/CMakeLists.txt | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/camera/CMakeLists.txt b/camera/CMakeLists.txt index 5cf56d09..4c7dfe94 100644 --- a/camera/CMakeLists.txt +++ b/camera/CMakeLists.txt @@ -1,18 +1,13 @@ # ############################################################################## # CMake configuration stuff -cmake_minimum_required(VERSION 3.14) -cmake_policy(SET CMP0057 NEW) enable_language(C CXX ASM) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -string(REPLACE "-MD" "-MMD" CMAKE_DEPFILE_FLAGS_C ${CMAKE_DEPFILE_FLAGS_C}) - # ############################################################################## # Target name set(LIB_NAME lib_camera) -set(LIB_NAME_ALIAS lib_camera_general) # Source files file(GLOB_RECURSE SOURCES_C "*.c") @@ -22,6 +17,16 @@ file(GLOB_RECURSE SOURCES_ASM "*.S") add_library(${LIB_NAME} STATIC) +target_include_directories(${LIB_NAME} PUBLIC . api) + +target_sources(${LIB_NAME} + PRIVATE + ${SOURCES_C} + ${SOURCES_XC} + ${SOURCES_CPP} + ${SOURCES_ASM} +) + target_compile_options(${LIB_NAME} PUBLIC -Os @@ -29,18 +34,5 @@ target_compile_options(${LIB_NAME} -fxscope -mcmodel=large ) -target_include_directories(${LIB_NAME} PUBLIC . api) - -target_sources(${LIB_NAME} PRIVATE ${SOURCES_C} ${SOURCES_XC} ${SOURCES_CPP} - $<$:${SOURCES_ASM}>) - - -# ############################################################################## -# Add and link the library -add_library(${LIB_NAME_ALIAS} INTERFACE) - target_link_libraries(${LIB_NAME} PUBLIC mipi::lib_mipi sensors::lib_imx) - -target_link_libraries(${LIB_NAME_ALIAS} INTERFACE ${LIB_NAME}) - -add_library(camera::lib_camera ALIAS ${LIB_NAME_ALIAS}) +add_library(camera::lib_camera ALIAS ${LIB_NAME}) From 5b05a301580a91456ef25adaaa9cd8190f16db48 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Mon, 5 Jun 2023 12:32:04 +0100 Subject: [PATCH 033/306] minor changes --- camera/CMakeLists.txt | 2 +- camera/api/user_api.h | 2 +- camera/src/image_hfilter.c | 13 +++------- camera/src/image_vfilter.c | 5 +--- camera/src/isp.c | 6 ++--- camera/src/statistics.c | 23 ++++++++--------- camera/src/user_api.c | 4 ++- examples/take_picture_downsample/src/app.c | 9 ++++--- sensors/api/sensor.h | 8 +----- sensors/imx219.h | 30 +++++++++++++--------- sensors/sensor_control.xc | 2 +- 11 files changed, 49 insertions(+), 55 deletions(-) diff --git a/camera/CMakeLists.txt b/camera/CMakeLists.txt index 4c7dfe94..c1221af9 100644 --- a/camera/CMakeLists.txt +++ b/camera/CMakeLists.txt @@ -17,7 +17,7 @@ file(GLOB_RECURSE SOURCES_ASM "*.S") add_library(${LIB_NAME} STATIC) -target_include_directories(${LIB_NAME} PUBLIC . api) +target_include_directories(${LIB_NAME} PUBLIC api) target_sources(${LIB_NAME} PRIVATE diff --git a/camera/api/user_api.h b/camera/api/user_api.h index 1ece61cc..1d66ac2f 100644 --- a/camera/api/user_api.h +++ b/camera/api/user_api.h @@ -6,7 +6,7 @@ #define H APP_IMAGE_HEIGHT_PIXELS #define W APP_IMAGE_WIDTH_PIXELS -#define RAW_CAPTURE 1 +// #define RAW_CAPTURE 0 // Image structure typedef struct { diff --git a/camera/src/image_hfilter.c b/camera/src/image_hfilter.c index f15fe0e8..269bc0d1 100644 --- a/camera/src/image_hfilter.c +++ b/camera/src/image_hfilter.c @@ -21,7 +21,7 @@ // must be zero, in each case. #define HFILTER_INPUT_STRIDE (APP_DECIMATION_FACTOR) -// float hfilter_coef[3] = { 0.20872991, 0.58254019, 0.20872991 }; +//Note: for filter coefficients reference : python/filters.txt #define A (0x1B) #define B (0x4B) @@ -41,14 +41,9 @@ const int8_t hfilter_coef_bayered_odd[32] = { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, }; - -//#define RED_GAIN 1 -//#define GREEN_GAIN 1 -//#define BLUE_GAIN 1 - -#define RED_GAIN 1//1.3 -#define GREEN_GAIN 0.8//1 -#define BLUE_GAIN 1//1.3 +#define RED_GAIN 1.3 +#define GREEN_GAIN 0.8 +#define BLUE_GAIN 1.3 int8_t hfilter_red[32] = { 0x1B,0x00,0x4B,0x00,0x1B,0x00,0x00,0x00, diff --git a/camera/src/image_vfilter.c b/camera/src/image_vfilter.c index 767f8a8e..ea7347d5 100644 --- a/camera/src/image_vfilter.c +++ b/camera/src/image_vfilter.c @@ -7,10 +7,7 @@ #define USE_SIMPLE_FILTER 0 -// float vfilter_coef[5] = { -// 0.0248892, 0.2528858, 0.44445, 0.2528858, 0.0248892 -// }; - +//Note: for filter coefficients reference : python/filters.txt #if USE_SIMPLE_FILTER diff --git a/camera/src/isp.c b/camera/src/isp.c index 8ff091a9..8785695d 100644 --- a/camera/src/isp.c +++ b/camera/src/isp.c @@ -15,7 +15,7 @@ float AE_compute_mean_skewness(global_stats_t *gstats){ sk += (*gstats)[0].skewness; sk += (*gstats)[1].skewness; sk += (*gstats)[2].skewness; - sk = sk / 3; + sk = sk / 3.0; return sk; } @@ -42,7 +42,7 @@ uint8_t AE_compute_new_exposure(float exposure, float skewness) c = b - fb*((b - a)/(fb - fa)); // each X samples, restart AE algorithm - if (count < 20){ + if (count < 5){ count = count + 1; } else{ @@ -53,7 +53,7 @@ uint8_t AE_compute_new_exposure(float exposure, float skewness) b = 80; fb = 1; } - return (uint8_t)c; + return c; } diff --git a/camera/src/statistics.c b/camera/src/statistics.c index 5dc0e61b..5e8f305a 100644 --- a/camera/src/statistics.c +++ b/camera/src/statistics.c @@ -33,9 +33,12 @@ void update_histogram( } + /** - * //TODO - */ +* Compute skewness of channel. +* This is used by auto exposure +* @param stats - * Pointer to channel statistics to update. +*/ void compute_skewness(channel_stats_t *stats) { const float zk_values[] = { @@ -61,9 +64,11 @@ void compute_skewness(channel_stats_t *stats) } + /** - * //TODO - */ +* Compute simple statistics for a set of data. +* @param stats - * Pointer to the channel statistics to be computed +*/ void compute_simple_stats(channel_stats_t *stats) { // Calculate the histogram @@ -100,9 +105,8 @@ void find_percentile(channel_stats_t *stats, const float fraction) stats -> percentile = (uint8_t) result; } -//////////////////////////// - +// Main thread for the statistics void statistics_thread( streaming_chanend_t c_img_in, CLIENT_INTERFACE(sensor_control_if, sc_if)) @@ -148,6 +152,7 @@ void statistics_thread( else{ // adjust exposure new_exp = AE_compute_new_exposure((float) new_exp, sk); + printf("new exp = %d\n", new_exp); sensor_control_set_exposure(sc_if, (uint8_t) new_exp); } @@ -155,12 +160,6 @@ void statistics_thread( AWB_compute_gains(&global_stats, &awb_gains); AWB_print_gains(&awb_gains); - // Adjust AWB - //float alfa = 255.0/global_stats[0].percentile; // RED - //float beta = 255.0/global_stats[1].percentile; // GREEN - //float gamma = 255.0/global_stats[2].percentile; // BLUE - //printf("awb:%f,%f,%f\n",alfa,beta,gamma); - } } diff --git a/camera/src/user_api.c b/camera/src/user_api.c index fd9843d9..6c70c055 100644 --- a/camera/src/user_api.c +++ b/camera/src/user_api.c @@ -12,7 +12,9 @@ static image_t *user_image; streaming_chanend_t c_user_api; #if (RAW_CAPTURE) - uint8_t image_raw[MIPI_IMAGE_HEIGHT_PIXELS*MIPI_LINE_WIDTH_BYTES]; + static uint8_t image_raw[MIPI_IMAGE_HEIGHT_PIXELS*MIPI_LINE_WIDTH_BYTES]; +#else + static uint8_t image_raw[0]; // Not needed #endif diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index 04820072..c66bd261 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -8,10 +8,11 @@ void user_app(streaming_chanend_t c_cam_api){ int8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; uint8_t out_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; - for(int8_t k = 0; k < APP_IMAGE_CHANNEL_COUNT; k++){ - memset(&image_buffer[k][0][0], -128, APP_IMAGE_HEIGHT_PIXELS * APP_IMAGE_WIDTH_PIXELS); // important to set to 0 in int8 (-128) - } - + + // set the input image to 0 + memset(image_buffer, -128, APP_IMAGE_CHANNEL_COUNT * APP_IMAGE_HEIGHT_PIXELS * APP_IMAGE_WIDTH_PIXELS); + + // Wait for the image to set exposure delay_milliseconds(5000); printf("Requesting image...\n"); camera_capture_image(image_buffer, c_cam_api); diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index c89a4aee..9d62f6b7 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -92,12 +92,6 @@ // ----------------------- Settings dependant of each sensor library -// Camera functions to be called from main program -#define camera_init(x) imx219_init(x) -#define camera_start(x) imx219_stream_start(x) -#define camera_configure(x) imx219_configure_mode(x) -#define camera_set_exposure(iic,ex) imx219_set_gain_dB(iic,ex) - // Camera dependant (do not edit) #define MIPI_LINE_WIDTH_BYTES MIPI_IMAGE_WIDTH_BYTES #define MIPI_MAX_PKT_SIZE_BYTES ((MIPI_LINE_WIDTH_BYTES) + 4) @@ -164,4 +158,4 @@ #endif -#endif // sensor_H \ No newline at end of file +#endif // sensor_H diff --git a/sensors/imx219.h b/sensors/imx219.h index 306b98d2..f95758eb 100644 --- a/sensors/imx219.h +++ b/sensors/imx219.h @@ -4,19 +4,18 @@ #include #include "i2c.h" +#include "xccompat.h" // I2C adress #define IMX219_I2C_ADDR 0x10 -// TODO maybe out of here +// Imx settings typedef struct { uint16_t addr; uint16_t val; } imx219_settings_t; -#ifdef __XC__ - // configure registers #if ((CONFIG_MODE == 0) || (CONFIG_MODE == 1)) #define CONFIG_REG mode_640_480_regs @@ -55,13 +54,20 @@ typedef struct // functions -int imx219_init(client interface i2c_master_if i2c); -int imx219_stream_start(client interface i2c_master_if i2c); -int imx219_configure_mode(client interface i2c_master_if i2c); -int imx219_stream_stop(client interface i2c_master_if i2c); -int imx219_set_gain_dB(client interface i2c_master_if i2c, uint32_t dBGain); -int imx219_set_binning(client interface i2c_master_if i2c, uint32_t H_binning, uint32_t V_binning); -int imx219_read(client interface i2c_master_if i2c, uint16_t addr); -void imx219_read_gains(client interface i2c_master_if i2c, uint16_t values[5]); +int imx219_init(CLIENT_INTERFACE(i2c_master_if, i2c)); +int imx219_stream_start(CLIENT_INTERFACE(i2c_master_if, i2c)); +int imx219_configure_mode(CLIENT_INTERFACE(i2c_master_if, i2c)); +int imx219_stream_stop(CLIENT_INTERFACE(i2c_master_if, i2c)); +int imx219_set_gain_dB(CLIENT_INTERFACE(i2c_master_if, i2c), uint32_t dBGain); +int imx219_set_binning(CLIENT_INTERFACE(i2c_master_if, i2c), uint32_t H_binning, uint32_t V_binning); +int imx219_read(CLIENT_INTERFACE(i2c_master_if, i2c), uint16_t addr); +void imx219_read_gains(CLIENT_INTERFACE(i2c_master_if, i2c), uint16_t values[5]); + + +#define camera_init(X) imx219_init(X) +#define camera_start(X) imx219_stream_start(X) +#define camera_configure(X) imx219_configure_mode(X) +#define camera_set_exposure(iic,ex) imx219_set_gain_dB(iic,ex) + + -#endif \ No newline at end of file diff --git a/sensors/sensor_control.xc b/sensors/sensor_control.xc index 26471f59..f82cc184 100644 --- a/sensors/sensor_control.xc +++ b/sensors/sensor_control.xc @@ -25,4 +25,4 @@ void sensor_control_set_exposure( unsigned exposure) { sc.set_exposure(exposure); -} \ No newline at end of file +} From 4d1fd67cadbf756de33834fb08f689b261042d1a Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Mon, 5 Jun 2023 14:53:35 +0100 Subject: [PATCH 034/306] changing preprocessor directive raw/downsample by function duplication --- camera/api/camera.h | 10 ++++ camera/api/image_vfilter.h | 4 -- camera/api/packet_handler.h | 7 ++- camera/api/user_api.h | 6 +++ camera/api/utils.h | 3 +- camera/src/camera.xc | 61 ++++++++++++++++++++++++- camera/src/image_vfilter.c | 30 ------------ camera/src/packet_handler.c | 55 +++++++++++++++++----- camera/src/statistics.c | 2 + camera/src/user_api.c | 27 +++++++---- camera/src/utils.c | 4 +- examples/take_picture_raw/src/app_raw.c | 29 ++++++++++++ examples/take_picture_raw/src/app_raw.h | 7 +++ examples/take_picture_raw/src/main.xc | 6 ++- 14 files changed, 188 insertions(+), 63 deletions(-) create mode 100644 examples/take_picture_raw/src/app_raw.c create mode 100644 examples/take_picture_raw/src/app_raw.h diff --git a/camera/api/camera.h b/camera/api/camera.h index ee122e9d..ef9f0ec4 100644 --- a/camera/api/camera.h +++ b/camera/api/camera.h @@ -46,4 +46,14 @@ void camera_main( client interface i2c_master_if i2c, streaming chanend c_user_api); +void camera_main_raw( + tileref mipi_tile, + in port p_mipi_clk, + in port p_mipi_rxa, + in port p_mipi_rxv, + buffered in port:32 p_mipi_rxd, + clock clk_mipi, + client interface i2c_master_if i2c, + streaming chanend c_user_api); + #endif //__XC__ diff --git a/camera/api/image_vfilter.h b/camera/api/image_vfilter.h index afd8ee8d..962cad2a 100644 --- a/camera/api/image_vfilter.h +++ b/camera/api/image_vfilter.h @@ -66,10 +66,6 @@ unsigned image_vfilter_process_row( vfilter_acc_t acc[], const int8_t pixel_data[]); -// unsigned image_vfilter_process_row_null( -// int8_t output[], -// vfilter_acc_t acc[]); - unsigned image_vfilter_drain( int8_t output[], vfilter_acc_t acc[]); diff --git a/camera/api/packet_handler.h b/camera/api/packet_handler.h index d0a5e0b6..1b874515 100644 --- a/camera/api/packet_handler.h +++ b/camera/api/packet_handler.h @@ -25,7 +25,12 @@ void mipi_packet_handler( streaming_chanend_t c_out_row, streaming_chanend_t c_user_api); - +void mipi_packet_handler_raw( + streaming_chanend_t c_pkt, + streaming_chanend_t c_ctrl, + streaming_chanend_t c_out_row, + streaming_chanend_t c_user_api); + #ifdef __XC__ } #endif diff --git a/camera/api/user_api.h b/camera/api/user_api.h index 1d66ac2f..7ccfdb47 100644 --- a/camera/api/user_api.h +++ b/camera/api/user_api.h @@ -6,6 +6,9 @@ #define H APP_IMAGE_HEIGHT_PIXELS #define W APP_IMAGE_WIDTH_PIXELS +#define H_RAW MIPI_IMAGE_WIDTH_PIXELS +#define W_RAW MIPI_IMAGE_HEIGHT_PIXELS + // #define RAW_CAPTURE 0 // Image structure @@ -26,6 +29,9 @@ void camera_api_request_update( const int8_t image_row[CH][W], const unsigned row_index); + + // RAW +unsigned camera_capture_image_raw(int8_t image_buff[H_RAW * W_RAW], streaming_chanend_t c_cam_api); void camera_api_request_update_raw(uint16_t line_number, uint8_t *img_row_ptr); void camera_api_request_complete_raw(); diff --git a/camera/api/utils.h b/camera/api/utils.h index a3019724..e83561da 100644 --- a/camera/api/utils.h +++ b/camera/api/utils.h @@ -23,7 +23,8 @@ void write_image( uint8_t image[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); void c_memcpy(void *dst, void *src, size_t size); void writeBMP(const char *filename, uint8_t img[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); -void write_image_raw(const char* filename,uint8_t *image); + +void write_image_raw(const char* filename, int8_t *image); #if defined(__XC__) || defined(__cplusplus) diff --git a/camera/src/camera.xc b/camera/src/camera.xc index 232b3328..fc54e0b5 100644 --- a/camera/src/camera.xc +++ b/camera/src/camera.xc @@ -58,7 +58,7 @@ void camera_main( // Start camera and its configurations int r = 0; r |= camera_init(i2c); - delay_milliseconds(100); //TODO include this inside the function + delay_milliseconds(100); r |= camera_configure(i2c); delay_milliseconds(600); r |= camera_start(i2c); @@ -73,3 +73,62 @@ void camera_main( sensor_control(sc_if, i2c); } } + + + +void camera_main_raw( + tileref mipi_tile, + in port p_mipi_clk, + in port p_mipi_rxa, + in port p_mipi_rxv, + buffered in port:32 p_mipi_rxd, + clock clk_mipi, + client interface i2c_master_if i2c, + streaming chanend c_user_api) +{ + + streaming chan c_pkt; + streaming chan c_ctrl; + streaming chan c_stat_thread; + + sensor_control_if sc_if; + // chan c_sensor_control; + + // See AN for MIPI shim + // 0x7E42 >> 0111 1110 0100 0010 + // in the explorer BOARD DPDN is swap + write_node_config_reg(mipi_tile, + XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, + 0x7E42); //TODO decompose into different values + + // send packet to MIPI shim + MipiPacketRx_init(mipi_tile, + p_mipi_rxd, + p_mipi_rxv, + p_mipi_rxa, + p_mipi_clk, + clk_mipi, + DEMUX_EN, + DEMUX_DATATYPE, + DEMUX_MODE, + MIPI_CLK_DIV, + MIPI_CFG_CLK_DIV); + + // Start camera and its configurations + int r = 0; + r |= camera_init(i2c); + delay_milliseconds(100); + r |= camera_configure(i2c); + delay_milliseconds(600); + r |= camera_start(i2c); + delay_milliseconds(2000); + + // start the different jobs (packet controller, handler, and post_process) + par + { + MipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); + mipi_packet_handler_raw(c_pkt, c_ctrl, c_stat_thread, c_user_api); + statistics_thread(c_stat_thread, sc_if); + sensor_control(sc_if, i2c); + } +} diff --git a/camera/src/image_vfilter.c b/camera/src/image_vfilter.c index ea7347d5..3f812fa0 100644 --- a/camera/src/image_vfilter.c +++ b/camera/src/image_vfilter.c @@ -124,36 +124,6 @@ unsigned image_vfilter_process_row( return 0; } -/** - * - * - */ -unsigned image_vfilter_process_row_null( - int8_t output[], - vfilter_acc_t acc[]) -{ - for(int k = 0; k < VFILTER_ACC_COUNT; k++){ - acc[k].next_tap++; - } - - for(int k = 0; k < VFILTER_ACC_COUNT; k++){ - if(acc[k].next_tap != VFILTER_TAP_COUNT) continue; - - // produce an output row from accumulator - pixel_vfilter_complete(output, - acc[k].buff, - vfilter_shift, - APP_IMAGE_WIDTH_PIXELS); - - // reset the accumulator - image_vfilter_reset(&acc[k]); - - return 1; - } - - return 0; -} - /** * After the last line of the image, some of the accumulators will be midway * through processing the image but still need to be output without maccing diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index d763161a..86c791ac 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -264,11 +264,6 @@ void handle_packet_raw( streaming_chanend_t c_out_row) { - __attribute__((aligned(8))) - static int8_t output_buff[2][APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_WIDTH_PIXELS]; - static unsigned out_dex = 0; - - // definitions const mipi_header_t header = pkt->header; const mipi_data_type_t data_type = MIPI_GET_DATA_TYPE(header); @@ -295,7 +290,6 @@ void handle_packet_raw( case MIPI_DT_FRAME_END: handle_frame_end_raw(); - out_dex = 1 - out_dex; break; case MIPI_EXPECTED_FORMAT: @@ -306,7 +300,6 @@ void handle_packet_raw( break; default: - // We've received a packet we don't know how to interpret. handle_unknown_packet(pkt); break; } @@ -350,17 +343,55 @@ void mipi_packet_handler( //const mipi_data_type_t data_type = MIPI_GET_DATA_TYPE(header); // unsigned time_start = measure_time(); - #if (RAW_CAPTURE) - handle_packet_raw(pkt, c_out_row); - #else - handle_packet(pkt, c_out_row); - #endif + handle_packet(pkt, c_out_row); // unsigned time_proc = measure_time() - time_start; } } +/** + * Top level of the packet handling thread. Receives MIPI packets from the + * packet receiver and passes them to `handle_packet()` for parsing and + * processing. + */ +void mipi_packet_handler_raw( + streaming_chanend_t c_pkt, + streaming_chanend_t c_ctrl, + streaming_chanend_t c_out_row, + streaming_chanend_t c_user_api) +{ + /* + * These buffers will be used to hold received MIPI packets while they're + * being processed. + */ + __attribute__((aligned(8))) + mipi_packet_t packet_buffer[MIPI_PKT_BUFFER_COUNT]; + unsigned pkt_idx = 0; + + camera_api_init(c_user_api); + + // Give the MIPI packet receiver a first buffer + s_chan_out_word(c_pkt, (unsigned) &packet_buffer[pkt_idx] ); + + while(1) { + pkt_idx = (pkt_idx + 1) & (MIPI_PKT_BUFFER_COUNT-1); + + mipi_packet_t * pkt = (mipi_packet_t*) s_chan_in_word(c_pkt); + // Swap buffers with the receiver thread. Give it the next buffer + // to fill and take the last filled buffer from it. + s_chan_out_word(c_pkt, (unsigned) &packet_buffer[pkt_idx] ); + + // Process the packet + //const mipi_header_t header = pkt->header; + //const mipi_data_type_t data_type = MIPI_GET_DATA_TYPE(header); + + // unsigned time_start = measure_time(); + handle_packet_raw(pkt, c_out_row); + // unsigned time_proc = measure_time() - time_start; + } +} + // NOTES //[1] diff --git a/camera/src/statistics.c b/camera/src/statistics.c index 5e8f305a..86d3021c 100644 --- a/camera/src/statistics.c +++ b/camera/src/statistics.c @@ -125,6 +125,7 @@ void statistics_thread( if(row == NULL) break; + // Update histogram for(uint8_t channel = 0; channel < APP_IMAGE_CHANNEL_COUNT; channel++){ update_histogram( &global_stats[channel].histogram, @@ -132,6 +133,7 @@ void statistics_thread( ); } } + // End of frame for(uint8_t channel = 0; channel < APP_IMAGE_CHANNEL_COUNT; channel++){ compute_skewness(&global_stats[channel]); diff --git a/camera/src/user_api.c b/camera/src/user_api.c index 6c70c055..5833a81c 100644 --- a/camera/src/user_api.c +++ b/camera/src/user_api.c @@ -8,15 +8,12 @@ #include "utils.h" #include "user_api.h" +// Pointers to both downsampled or raw static image_t *user_image; -streaming_chanend_t c_user_api; - -#if (RAW_CAPTURE) - static uint8_t image_raw[MIPI_IMAGE_HEIGHT_PIXELS*MIPI_LINE_WIDTH_BYTES]; -#else - static uint8_t image_raw[0]; // Not needed -#endif +static int8_t *image_raw_ptr; +// In order to interface the handler and api +streaming_chanend_t c_user_api; // ---------------------------------------------------------------- void camera_api_init( @@ -77,14 +74,24 @@ void camera_api_request_complete() // ---------------------------------------------------------------- +unsigned camera_capture_image_raw( + int8_t image_buff[H_RAW*W_RAW], + streaming_chanend_t c_cam_api +) +{ + image_raw_ptr = (int8_t*)&image_buff[0]; + c_user_api = c_cam_api; + unsigned tmp = s_chan_in_word(c_cam_api); + return tmp; +} + void camera_api_request_update_raw(uint16_t line_number, uint8_t* img_row_ptr){ uint32_t pos = (line_number) * MIPI_LINE_WIDTH_BYTES; - c_memcpy((void*) &image_raw[pos], + c_memcpy((void*) &image_raw_ptr[pos], (void*) &img_row_ptr[0], MIPI_LINE_WIDTH_BYTES); } void camera_api_request_complete_raw(){ - write_image_raw("capture.bin", image_raw); - exit(0); + s_chan_out_word(c_user_api, 1); } diff --git a/camera/src/utils.c b/camera/src/utils.c index 55cd5df7..9ba76d34 100644 --- a/camera/src/utils.c +++ b/camera/src/utils.c @@ -136,14 +136,14 @@ void writeBMP(const char* filename, uint8_t img[APP_IMAGE_CHANNEL_COUNT][APP_IMA // Write image to disk. This is called by camera main () to do the work void write_image_raw( const char* filename, - uint8_t *image) + int8_t *image) { static FILE* img_file = NULL; img_file = fopen(filename, "wb"); for(uint16_t k = 0; k < MIPI_IMAGE_HEIGHT_PIXELS; k++){ for(uint16_t j = 0; j < MIPI_LINE_WIDTH_BYTES; j++){ uint32_t pos = k * MIPI_LINE_WIDTH_BYTES + j; - fwrite(&image[pos], sizeof(uint8_t), 1, img_file); + fwrite(&image[pos], sizeof(int8_t), 1, img_file); } } fclose(img_file); diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c new file mode 100644 index 00000000..3a8e4871 --- /dev/null +++ b/examples/take_picture_raw/src/app_raw.c @@ -0,0 +1,29 @@ +// std +#include +// xcore +#include +#include +// user +#include "mipi.h" +#include "utils.h" +#include "user_api.h" +#include "app_raw.h" + +void user_app_raw(streaming_chanend_t c_cam_api){ + + // set the input image to 0 + int8_t image_buffer[H_RAW*W_RAW]; + memset(image_buffer, -128, H_RAW*W_RAW); + + // wait for set the camera + delay_milliseconds(500); + + // Request an image + printf("Requesting image...\n"); + camera_capture_image_raw(image_buffer, c_cam_api); + printf("Image captured...\n"); + + // Save the image to a file + write_image_raw("capture.bin", image_buffer); + exit(0); +} diff --git a/examples/take_picture_raw/src/app_raw.h b/examples/take_picture_raw/src/app_raw.h new file mode 100644 index 00000000..0ccad37e --- /dev/null +++ b/examples/take_picture_raw/src/app_raw.h @@ -0,0 +1,7 @@ +#include "xs1.h" +#include "platform.h" +#include "xccompat.h" + +#include "camera.h" + +void user_app_raw(streaming_chanend_t c_cam_api); diff --git a/examples/take_picture_raw/src/main.xc b/examples/take_picture_raw/src/main.xc index 7bc95048..9ad619ed 100644 --- a/examples/take_picture_raw/src/main.xc +++ b/examples/take_picture_raw/src/main.xc @@ -5,7 +5,7 @@ #include "i2c.h" #include "camera.h" - +#include "app_raw.h" // I2C interface ports #define Kbps 400 @@ -31,7 +31,7 @@ int main(void) par { on tile[0]: i2c_master(i2c, 1, p_scl, p_sda, Kbps); - on tile[MIPI_TILE]: camera_main(tile[MIPI_TILE], + on tile[MIPI_TILE]: camera_main_raw(tile[MIPI_TILE], p_mipi_clk, p_mipi_rxa, p_mipi_rxv, @@ -39,6 +39,8 @@ int main(void) clk_mipi, i2c[0], c_cam_api); + + on tile[MIPI_TILE]: user_app_raw(c_cam_api); } return 0; } From 2b1af6855105a8b8990d01728f7c77dd037f8584 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 6 Jun 2023 11:32:40 +0100 Subject: [PATCH 035/306] adding initial xscope_fileio --- .gitmodules | 3 ++ CMakeLists.txt | 6 +++ examples/config.xscope | 11 ++++-- examples/simple_timing/CMakeLists.txt | 6 ++- examples/simple_timing/src/main.xc | 11 +++++- examples/simple_timing/src/mipi_timing.xc | 8 ++-- examples/simple_timing/src/process_frame.c | 43 ---------------------- examples/simple_timing/src/process_frame.h | 16 -------- examples/take_picture/CMakeLists.txt | 6 ++- examples/take_picture/src/mipi_main.xc | 8 ++-- utils/CMakeLists.txt | 11 ++++++ utils/common_utils/utils.c | 19 ++++++++++ utils/common_utils/utils.h | 19 ++++++++++ utils/xscope_fileio | 1 + utils/xscope_fileio.cmake | 18 +++++++++ 15 files changed, 112 insertions(+), 74 deletions(-) delete mode 100644 examples/simple_timing/src/process_frame.c delete mode 100644 examples/simple_timing/src/process_frame.h create mode 100644 utils/CMakeLists.txt create mode 100644 utils/common_utils/utils.c create mode 100644 utils/common_utils/utils.h create mode 160000 utils/xscope_fileio create mode 100644 utils/xscope_fileio.cmake diff --git a/.gitmodules b/.gitmodules index 0408c2c4..5511d4e6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -18,3 +18,6 @@ url = git@github.com:xmos/Unity # unit testing branch = develop +[submodule "utils/xscope_fileio"] + path = utils/xscope_fileio + url = https://github.com/xmos/xscope_fileio diff --git a/CMakeLists.txt b/CMakeLists.txt index b4c3cd34..8844904c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,10 +25,16 @@ add_subdirectory(sensors) ## Add cameras add_subdirectory(camera) +if(PROJECT_IS_TOP_LEVEL) +## utils for examples and testing +add_subdirectory(utils) + ## Add tests add_subdirectory(tests) ## Add examples add_subdirectory(examples) +endif() # top level + endif() # xs3a diff --git a/examples/config.xscope b/examples/config.xscope index 69236026..d81166cf 100644 --- a/examples/config.xscope +++ b/examples/config.xscope @@ -15,8 +15,11 @@ - - - - + + + + + + + diff --git a/examples/simple_timing/CMakeLists.txt b/examples/simple_timing/CMakeLists.txt index fdd110ad..e2dacd30 100644 --- a/examples/simple_timing/CMakeLists.txt +++ b/examples/simple_timing/CMakeLists.txt @@ -5,7 +5,7 @@ # <--- Set the executable set(TARGET example_timing) -file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.*c) +file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.xc) set(APP_INCLUDES ${CMAKE_CURRENT_LIST_DIR}/src ) @@ -48,7 +48,9 @@ set(APP_COMMON_LINK_LIBRARIES core::general mipi::lib_mipi i2c::lib_i2c - sensors::lib_imx + sensors::lib_imx + fwk_camera::utils + fwk_camera::xscope_fileio ) diff --git a/examples/simple_timing/src/main.xc b/examples/simple_timing/src/main.xc index 77977616..a3b4941b 100644 --- a/examples/simple_timing/src/main.xc +++ b/examples/simple_timing/src/main.xc @@ -6,6 +6,10 @@ #include "i2c.h" #include "mipi_timing.h" +extern "C" { +#include "xscope_io_device.h" +} + // I2C interface ports #define Kbps 400 on tile[0]: port p_scl = XS1_PORT_1N; @@ -13,10 +17,15 @@ on tile[0]: port p_sda = XS1_PORT_1O; int main(void) { + chan xscope_chan; i2c_master_if i2c[1]; par { + xscope_host_data(xscope_chan); on tile[0]: i2c_master(i2c, 1, p_scl, p_sda, Kbps); - on tile[MIPI_TILE]: mipi_main(i2c[0]); + on tile[MIPI_TILE]: { + xscope_io_init(xscope_chan); + mipi_main(i2c[0]); + } } return 0; } diff --git a/examples/simple_timing/src/mipi_timing.xc b/examples/simple_timing/src/mipi_timing.xc index 0add1fc0..50765329 100644 --- a/examples/simple_timing/src/mipi_timing.xc +++ b/examples/simple_timing/src/mipi_timing.xc @@ -36,10 +36,12 @@ #define PRINT_TIME(y) printf("%d\n", y) #define MS_MULTIPLIER 0.00001 -// Image -#include "process_frame.h" +// Utils +#include "utils.h" // Globals +#define FINAL_IMAGE_FILENAME "out.raw" +uint8_t FINAL_IMAGE[MIPI_IMAGE_HEIGHT_PIXELS][MIPI_LINE_WIDTH_BYTES]; char end_transmission = 0; char found = 0; mipi_data_type_t p_data_type = 0; @@ -75,7 +77,7 @@ void save_image_to_file(chanend flag) case flag :> int i: { // write to a file - write_image(); + write_image(FINAL_IMAGE_FILENAME, &FINAL_IMAGE[0][0], MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES); delay_microseconds(200); // for stability exit(1); // end the program here break; diff --git a/examples/simple_timing/src/process_frame.c b/examples/simple_timing/src/process_frame.c deleted file mode 100644 index 0b75aa72..00000000 --- a/examples/simple_timing/src/process_frame.c +++ /dev/null @@ -1,43 +0,0 @@ - -#include "process_frame.h" -#include -#include -#include - -#define FINAL_IMAGE_FILENAME "out.raw" -uint8_t FINAL_IMAGE[MIPI_IMAGE_HEIGHT_PIXELS][MIPI_LINE_WIDTH_BYTES]; - -// Write image to disk. This is called by camera main () to do the work -void write_image() -{ - static FILE* img_file = NULL; - img_file = fopen(FINAL_IMAGE_FILENAME, "wb"); - for(int k = 0; k < MIPI_IMAGE_HEIGHT_PIXELS; k++){ - for(int j = 0; j < MIPI_IMAGE_WIDTH_BYTES; j++){ - fwrite(&FINAL_IMAGE[k][j], sizeof(uint8_t), 1, img_file); - } - } - fclose(img_file); - printf("Outfile %s\n", FINAL_IMAGE_FILENAME); - printf("image size (%dx%d)\n", MIPI_LINE_WIDTH_BYTES, MIPI_IMAGE_HEIGHT_PIXELS); -} - - -// This is called when want to memcpy from Xc to C -void not_silly_memcpy( - void* dst, - void* src, - size_t size) -{ - memcpy(dst, src, size); -} - -/* -void user_input(void){ - printf("Enter the character 'c' to capture the frame : "); - int c = getchar(); - if(c != 99){ // == ascii "c" - exit(0); - } -} -*/ \ No newline at end of file diff --git a/examples/simple_timing/src/process_frame.h b/examples/simple_timing/src/process_frame.h deleted file mode 100644 index 2b6b7519..00000000 --- a/examples/simple_timing/src/process_frame.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include -#include "mipi_timing.h" - -#ifdef __XC__ -extern "C" { -#endif - -void write_image(); -void not_silly_memcpy(void *dst, void *src, size_t size); -// void user_input(void); - -#ifdef __XC__ -} -#endif \ No newline at end of file diff --git a/examples/take_picture/CMakeLists.txt b/examples/take_picture/CMakeLists.txt index e19ab2e1..41e7ab81 100644 --- a/examples/take_picture/CMakeLists.txt +++ b/examples/take_picture/CMakeLists.txt @@ -5,7 +5,7 @@ # <--- Set the executable set(TARGET example_take_picture) -file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.*c) +file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.xc) set(APP_INCLUDES ${CMAKE_CURRENT_LIST_DIR}/src ) @@ -49,7 +49,9 @@ set(APP_COMMON_LINK_LIBRARIES core::general mipi::lib_mipi i2c::lib_i2c - sensors::lib_imx + sensors::lib_imx + fwk_camera::utils + fwk_camera::xscope_fileio ) diff --git a/examples/take_picture/src/mipi_main.xc b/examples/take_picture/src/mipi_main.xc index fac21aac..a24475d7 100644 --- a/examples/take_picture/src/mipi_main.xc +++ b/examples/take_picture/src/mipi_main.xc @@ -20,10 +20,12 @@ #define MSG_SUCCESS "Stream start OK\n" #define MSG_FAIL "Stream start Failed\n" -// Image -#include "process_frame.h" +// Utils +#include "utils.h" // Globals +#define FINAL_IMAGE_FILENAME "out.raw" +uint8_t FINAL_IMAGE[MIPI_IMAGE_HEIGHT_PIXELS][MIPI_LINE_WIDTH_BYTES]; char end_transmission = 0; mipi_data_type_t p_data_type = 0; @@ -58,7 +60,7 @@ void save_image_to_file(chanend flag) case flag :> int i: { // write to a file - write_image(); + write_image(FINAL_IMAGE_FILENAME, &FINAL_IMAGE[0][0], MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES); delay_microseconds(200); // for stability //TODO maybe inside the function exit(1); // end the program here break; diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt new file mode 100644 index 00000000..2268adee --- /dev/null +++ b/utils/CMakeLists.txt @@ -0,0 +1,11 @@ + +include(xscope_fileio.cmake) + +set(LIB_NAME fwk_camera_utils) +add_library(${LIB_NAME} INTERFACE) + +target_sources(${LIB_NAME} INTERFACE common_utils/utils.c) + +target_include_directories(${LIB_NAME} INTERFACE common_utils) + +add_library(fwk_camera::utils ALIAS ${LIB_NAME}) diff --git a/utils/common_utils/utils.c b/utils/common_utils/utils.c new file mode 100644 index 00000000..658db7fe --- /dev/null +++ b/utils/common_utils/utils.c @@ -0,0 +1,19 @@ + +#include "utils.h" +#include + +void write_image(char * filename, uint8_t * image, const size_t height, const size_t width) +{ + xscope_file_t fp = xscope_open_file(filename, "wb"); + + xscope_fwrite(&fp, image, height * width * sizeof(uint8_t)); + + xscope_close_all_files(); + printf("Output file: %s\n", filename); + printf("Image dimentions: %d x %d\n", width, height); +} + +void not_silly_memcpy(void * dst, void * src, size_t size) +{ + memcpy(dst, src, size); +} diff --git a/utils/common_utils/utils.h b/utils/common_utils/utils.h new file mode 100644 index 00000000..1a5990e4 --- /dev/null +++ b/utils/common_utils/utils.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include +#include + +#ifdef __XC__ +extern "C" { +#endif + +#include "xscope_io_device.h" + +void write_image(char * filename, uint8_t * image, const size_t height, const size_t width); +void not_silly_memcpy(void * dst, void * src, size_t size); + +#ifdef __XC__ +} +#endif diff --git a/utils/xscope_fileio b/utils/xscope_fileio new file mode 160000 index 00000000..52cff082 --- /dev/null +++ b/utils/xscope_fileio @@ -0,0 +1 @@ +Subproject commit 52cff0826b2773beec49044a0729bb000c011379 diff --git a/utils/xscope_fileio.cmake b/utils/xscope_fileio.cmake new file mode 100644 index 00000000..f7c275d0 --- /dev/null +++ b/utils/xscope_fileio.cmake @@ -0,0 +1,18 @@ + +set(XSCOPE_FILEIO_PATH ${CMAKE_CURRENT_LIST_DIR}/xscope_fileio) + +set(LIB_NAME fwk_camera_xscope_fileio) +add_library(${LIB_NAME} INTERFACE) + +target_sources(${LIB_NAME} + INTERFACE + ${XSCOPE_FILEIO_PATH}/xscope_fileio/src/xscope_io_device.c +) + +target_include_directories(${LIB_NAME} + INTERFACE + ${XSCOPE_FILEIO_PATH}/xscope_fileio + ${XSCOPE_FILEIO_PATH}/xscope_fileio/api +) + +add_library(fwk_camera::xscope_fileio ALIAS ${LIB_NAME}) From 68c7c2dd295cdcf52172da3537e887fb6fa30d82 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Tue, 6 Jun 2023 16:48:09 +0100 Subject: [PATCH 036/306] * adding isp functions * minor changes : documentation & cleanup --- camera/api/isp.h | 20 +++- camera/api/utils.h | 6 + camera/src/image_hfilter.c | 77 ++++++------- camera/src/image_vfilter.c | 54 +++------ camera/src/isp.c | 126 +++++++++++++++++++++ camera/src/statistics.c | 5 +- camera/src/utils.c | 90 ++++++++++++--- examples/take_picture_downsample/src/app.c | 28 +---- examples/take_picture_downsample/src/app.h | 5 +- modules/mipi/src/MipiPacketRx.S | 4 +- python/decode_downsampled.py | 2 +- python/utils.py | 17 +-- 12 files changed, 299 insertions(+), 135 deletions(-) diff --git a/camera/api/isp.h b/camera/api/isp.h index 5a76f526..6b608022 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -25,5 +25,23 @@ typedef struct { void AWB_compute_gains(global_stats_t *gstats, AWB_gains_t *gains); void AWB_print_gains(AWB_gains_t *gains); +int8_t AWB_compute_filter_gain(int8_t coeff, float factor); -#endif + +// ---------------------------------- GAMMA ------------------------------ +extern const uint8_t gamma_1p8_s1[255]; +void isp_gamma_stride1(const uint32_t buffsize, uint8_t *img); + + +// -------------------------- ROTATE/RESIZE ------------------------------------- +void isp_bilinear_resize( + const uint16_t in_width, + const uint16_t in_height, + uint8_t *img, + const uint16_t out_width, + const uint16_t out_height, + uint8_t *out_img); + +void isp_rotate_image(const uint8_t *src, uint8_t *dest, int width, int height); + +#endif // ISP_H diff --git a/camera/api/utils.h b/camera/api/utils.h index e83561da..f3eae490 100644 --- a/camera/api/utils.h +++ b/camera/api/utils.h @@ -22,8 +22,14 @@ void write_image( const char* filename, uint8_t image[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); void c_memcpy(void *dst, void *src, size_t size); +void rotate_image(const char *filename, uint8_t image[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); void writeBMP(const char *filename, uint8_t img[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); +void img_int8_to_uint8( + int8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS], + uint8_t out_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS] +); + void write_image_raw(const char* filename, int8_t *image); diff --git a/camera/src/image_hfilter.c b/camera/src/image_hfilter.c index 269bc0d1..b0cff939 100644 --- a/camera/src/image_hfilter.c +++ b/camera/src/image_hfilter.c @@ -4,7 +4,7 @@ #include #include "image_hfilter.h" - +#include "isp.h" // The filter coefficients for the horizontal filter. Because the VPU (and in // particular, VLMACCR) is used, this needs to be 32 bytes long, padded with @@ -25,8 +25,11 @@ #define A (0x1B) #define B (0x4B) -#define C (0x64) +#define C (0x1B) +#define D (0x4B) + +/* const int8_t hfilter_coef_bayered_even[32] = { 0x1B,0x00,0x4B,0x00,0x1B,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, @@ -40,25 +43,28 @@ const int8_t hfilter_coef_bayered_odd[32] = { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, }; +*/ -#define RED_GAIN 1.3 -#define GREEN_GAIN 0.8 -#define BLUE_GAIN 1.3 +#define AWB_gain_RED 1.3 +#define AWB_gain_BLUE 0.8 +#define AWB_gain_GREEN 1.3 -int8_t hfilter_red[32] = { - 0x1B,0x00,0x4B,0x00,0x1B,0x00,0x00,0x00, +int8_t hfilter_red[32] = { + C,0x00,D,0x00,C,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, }; + int8_t hfilter_green[32] = { - 0x1B,0x00,0x4B,0x00,0x1B,0x00,0x00,0x00, + 0x00,A,0x00,B,0x00,A,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, }; -int8_t hfilter_blue[32] = { - 0x1B,0x00,0x4B,0x00,0x1B,0x00,0x00,0x00, + +int8_t hfilter_blue[32] = { + 0x00,C,0x00,D,0x00,C,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, @@ -66,24 +72,27 @@ int8_t hfilter_blue[32] = { /// ---------------------------------------------------------------- +const int16_t MIN = 0x0000; +const int16_t MIN2 = 0x0000; + const int16_t hfilter_acc_init[2][16] = { - { 0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000, - 0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000, }, - { 0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000, - 0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000, }, + { MIN,MIN,MIN,MIN,MIN,MIN,MIN,MIN, + MIN,MIN,MIN,MIN,MIN,MIN,MIN,MIN, }, + { MIN2,MIN2,MIN2,MIN2,MIN2,MIN2,MIN2,MIN2, + MIN2,MIN2,MIN2,MIN2,MIN2,MIN2,MIN2,MIN2, }, }; -const int16_t hfilter_shift[16] = {7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7}; - - -int8_t compute_gain(int8_t num, float f) { - float result = f * (num + 127.0f); - if (result >= 255.0f) { - return 127; - } else if (result <= 0.0f) { - return -127; - } - result -= 127.0f; - return result; + +const int16_t N = 7; +const int16_t hfilter_shift[16] = {N,N,N,N,N,N,N,N,N,N,N,N,N,N,N,N}; + +void apply_gains( + int8_t gains[3], + int8_t *filter, + const int8_t offset) +{ + filter[0+offset] = gains[0]; + filter[2+offset] = gains[1]; + filter[4+offset] = gains[2]; } @@ -92,18 +101,10 @@ void image_hfilter( const int8_t pix_in[SENSOR_RAW_IMAGE_WIDTH_PIXELS], const unsigned channel_index) { - // modify filters - hfilter_red[0] = compute_gain(A, RED_GAIN); - hfilter_red[2] = compute_gain(B, RED_GAIN); - hfilter_red[4] = compute_gain(A, RED_GAIN); - - hfilter_green[1] = compute_gain(A, GREEN_GAIN); - hfilter_green[3] = compute_gain(B, GREEN_GAIN); - hfilter_green[5] = compute_gain(A, GREEN_GAIN); - - hfilter_blue[1] = compute_gain(A, BLUE_GAIN); - hfilter_blue[3] = compute_gain(B, BLUE_GAIN); - hfilter_blue[5] = compute_gain(A, BLUE_GAIN); + // appply gains + // apply_gains({0x1B, 0x4B, 0x1B}, &hfilter_red[0], 0); + // apply_gains({0x1B, 0x4B, 0x1B}, &hfilter_green[0], 0); + // apply_gains({0x1B, 0x4B, 0x1B}, &hfilter_blue[0], 0); // group filters int8_t* channel_hfilter_coefs_rggb[3] = { diff --git a/camera/src/image_vfilter.c b/camera/src/image_vfilter.c index 3f812fa0..912b6505 100644 --- a/camera/src/image_vfilter.c +++ b/camera/src/image_vfilter.c @@ -5,47 +5,21 @@ #include "image_vfilter.h" -#define USE_SIMPLE_FILTER 0 - //Note: for filter coefficients reference : python/filters.txt - -#if USE_SIMPLE_FILTER - - static - const int32_t vfilter_acc_offset = 0; - - static - const int8_t vfilter_coef[5][16] = { - {0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1,0x1}, - {0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0}, - {0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0}, - {0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0}, - {0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0}, - }; - - static - const int16_t vfilter_shift[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,}; - -#else - - static - const int32_t vfilter_acc_offset = 0; - - static - const int8_t vfilter_coef[5][16] = { - { 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,}, - { 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65,}, - {114,114,114,114,114,114,114,114,114,114,114,114,114,114,114,114,}, - { 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65,}, - { 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,}, - }; - - static - const int16_t vfilter_shift[16] = {8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,}; - -#endif - - +static +const int32_t vfilter_acc_offset = 0; + +static +const int8_t vfilter_coef[5][16] = { + { 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,}, + { 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65,}, + {114,114,114,114,114,114,114,114,114,114,114,114,114,114,114,114,}, + { 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65,}, + { 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,}, +}; + +static +const int16_t vfilter_shift[16] = {8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,}; /** * Prepare the provided accumulator struct for accumulation. diff --git a/camera/src/isp.c b/camera/src/isp.c index 8785695d..f53a1fab 100644 --- a/camera/src/isp.c +++ b/camera/src/isp.c @@ -69,3 +69,129 @@ void AWB_compute_gains(global_stats_t *gstats, AWB_gains_t *gains){ void AWB_print_gains(AWB_gains_t *gains){ printf("awb:%f,%f,%f\n",gains->alfa,gains->beta,gains->gamma); } + +int8_t AWB_compute_filter_gain(int8_t coeff, float factor) { + // clip factor + const float maxf = 1.6; + const float minf = 1; + + if (factor > maxf){ + factor = maxf; + } + else if (factor <= minf){ + factor = minf; + } + + // compute the factor + float result = factor * (coeff + 128.0f); + if (result >= 255.0f) { + result = 127; + } else if (result <= 0.0f) { + result = -128; + } else{ + result -= 128; + } + return (int8_t)result; +} + + +// ---------------------------------- GAMMA ------------------------------ +/** +* Apply 1.8 gamma to each pixel in an image using stride 1 +* it take 255 bytes of memory, 0MUL, 0DIV, 2MEM ACCESS +* @param buffsize - Size of the buffer to operate on. +* @param img - * Pointer to the image to operate on. Modified +*/ +const uint8_t gamma_1p8_s1[255] = { +0,12,17,22,25,29,32,35,37,40,42,44,47,49,51,53,55,57,58,60,62,64,65,67,69, +70,72,73,75,76,78,79,80,82,83,85,86,87,89,90,91,92,94,95,96,97,98,100,101, +102,103,104,105,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121, +122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,139, +140,141,142,143,144,145,146,146,147,148,149,150,151,152,152,153,154,155,156, +157,157,158,159,160,161,161,162,163,164,165,165,166,167,168,169,169,170,171, +172,172,173,174,175,175,176,177,178,178,179,180,181,181,182,183,183,184,185, +186,186,187,188,188,189,190,191,191,192,193,193,194,195,195,196,197,198,198, +199,200,200,201,202,202,203,204,204,205,206,206,207,208,208,209,209,210,211, +211,212,213,213,214,215,215,216,217,217,218,218,219,220,220,221,222,222,223, +223,224,225,225,226,226,227,228,228,229,230,230,231,231,232,233,233,234,234, +235,236,236,237,237,238,238,239,240,240,241,241,242,243,243,244,244,245,245, +246,247,247,248,248,249,249,250,251,251,252,252,253,253,254,255}; + +void isp_gamma_stride1(const uint32_t buffsize, uint8_t *img){ + // gamma naming: 1p8_s1 = gamma 1.8 , with a stride of 1 + // 1p8_s4 => img^(1/1.8) (in a normalizeed 0-1 image) + for(uint32_t i=0; i #include -// XOR VERSION -// BYTEREV DISABLED +// XOR ENABLED +// BYTEREV DISABLED /**************************************************** **************************************************** diff --git a/python/decode_downsampled.py b/python/decode_downsampled.py index ba82dbf0..aee12e4e 100644 --- a/python/decode_downsampled.py +++ b/python/decode_downsampled.py @@ -39,4 +39,4 @@ plt.show() # show histograms -show_histogram_by_channel(img) \ No newline at end of file +show_histogram_by_channel(img) diff --git a/python/utils.py b/python/utils.py index 062bb29e..1e897cd9 100644 --- a/python/utils.py +++ b/python/utils.py @@ -464,15 +464,15 @@ def run_histogram_equalization(img_bgr): def show_histogram_by_channel(image): # Set the histogram bins to 256, the range to 0-255 - hist_size = 256 - hist_range = (0, 256) + hist_size = 260 + hist_range = (0, 255) # Plot the histograms using plt.hist plt.figure(figsize=(10, 5)) for i, col in enumerate(['r', 'g', 'b']): plt.subplot(1, 3, i+1) plt.title(f'{col.upper()} Histogram') - plt.xlim([0, 255]) + plt.xlim([0, hist_size]) plt.hist(image[:,:,i].ravel(), bins=hist_size, range=hist_range, color=col) plt.show() @@ -480,7 +480,7 @@ def show_histogram_by_channel(image): def show_histogram_by_channel_ax(image, ax): # Set the histogram bins to 256, the range to 0-255 hist_size = 256 - hist_range = (0, 256) + hist_range = (0, hist_size) # Plot the histograms using plt.hist for i, col in enumerate(['r', 'g', 'b']): @@ -489,6 +489,7 @@ def show_histogram_by_channel_ax(image, ax): def plot_imgs(img, img_raw_RGB, flip, ax=None): + hist_size = 256 if ax is None: fig, ax = plt.subplots(2, 2, figsize=(16, 8)) if flip: @@ -503,9 +504,9 @@ def plot_imgs(img, img_raw_RGB, flip, ax=None): # plt.show() # plot histogram - ax3.hist(img.mean(axis=2).flatten(), 255) - ax3.hist(img_raw_RGB.mean(axis=2).flatten(), 255) - ax3.axis(xmin=0,xmax=255) + ax3.hist(img.mean(axis=2).flatten(), hist_size) + ax3.hist(img_raw_RGB.mean(axis=2).flatten(), hist_size) + ax3.axis(xmin=0,xmax=hist_size) ax3.legend(["processed", "unprocessed"]) # show histogram for 3 channels @@ -888,4 +889,4 @@ def gray_world(img): return img if __name__ == '__main__': - pass \ No newline at end of file + pass From cd3b7658a7d61845ff626f31550e716939ea616d Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Wed, 7 Jun 2023 10:43:40 +0100 Subject: [PATCH 037/306] remove fwk_core dependency Fixes xmos/fwk_camera#25 --- examples/simple_timing/CMakeLists.txt | 3 +-- examples/take_picture_downsample/CMakeLists.txt | 3 +-- examples/take_picture_raw/CMakeLists.txt | 3 +-- modules/CMakeLists.txt | 1 - modules/core | 1 - 5 files changed, 3 insertions(+), 8 deletions(-) delete mode 160000 modules/core diff --git a/examples/simple_timing/CMakeLists.txt b/examples/simple_timing/CMakeLists.txt index fdd110ad..f6df09a6 100644 --- a/examples/simple_timing/CMakeLists.txt +++ b/examples/simple_timing/CMakeLists.txt @@ -44,8 +44,7 @@ set(APP_LINK_OPTIONS ) # <--- Link libraries -set(APP_COMMON_LINK_LIBRARIES - core::general +set(APP_COMMON_LINK_LIBRARIES mipi::lib_mipi i2c::lib_i2c sensors::lib_imx diff --git a/examples/take_picture_downsample/CMakeLists.txt b/examples/take_picture_downsample/CMakeLists.txt index 8660d172..e6be537d 100644 --- a/examples/take_picture_downsample/CMakeLists.txt +++ b/examples/take_picture_downsample/CMakeLists.txt @@ -45,8 +45,7 @@ set(APP_LINK_OPTIONS # <--- Link libraries -set(APP_COMMON_LINK_LIBRARIES - core::general +set(APP_COMMON_LINK_LIBRARIES mipi::lib_mipi i2c::lib_i2c sensors::lib_imx diff --git a/examples/take_picture_raw/CMakeLists.txt b/examples/take_picture_raw/CMakeLists.txt index d8e97883..de3cd654 100644 --- a/examples/take_picture_raw/CMakeLists.txt +++ b/examples/take_picture_raw/CMakeLists.txt @@ -45,8 +45,7 @@ set(APP_LINK_OPTIONS # <--- Link libraries -set(APP_COMMON_LINK_LIBRARIES - core::general +set(APP_COMMON_LINK_LIBRARIES mipi::lib_mipi i2c::lib_i2c sensors::lib_imx diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index cf309c8c..00d5d1a0 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -1,5 +1,4 @@ # adding submodules here -add_subdirectory(core) add_subdirectory(mipi) # lib_i2c and lib_xassert do not have CMakeLists.txt # so using wrappers here diff --git a/modules/core b/modules/core deleted file mode 160000 index da12554b..00000000 --- a/modules/core +++ /dev/null @@ -1 +0,0 @@ -Subproject commit da12554b3556b0114302ab9c37f6a602b4b5baf5 From cd0d01bc839fdf7d81415b1ab842a8b3d48f684a Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Wed, 7 Jun 2023 10:44:27 +0100 Subject: [PATCH 038/306] closes #25 --- .gitmodules | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitmodules b/.gitmodules index 0408c2c4..4e9eef05 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,10 +6,6 @@ path = modules/i2c url = git@github.com:xmos/lib_i2c # adding lib assert and cmake to the repo branch = develop -[submodule "modules/core"] - path = modules/core - url = git@github.com:xmos/fwk_core # xcore math inside the repo - branch = develop [submodule "modules/xassert"] path = modules/xassert url = git@github.com:xmos/lib_xassert.git From 0128d8b3e4d52776092d1c58664eed2e14b73ec0 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Wed, 7 Jun 2023 10:59:13 +0100 Subject: [PATCH 039/306] fisrt example --- examples/take_picture_raw/src/app_raw.c | 6 ++++-- utils/common_utils/utils.c | 6 +++--- utils/common_utils/utils.h | 19 ------------------- utils/common_utils/utils_new.h | 18 ++++++++++++++++++ 4 files changed, 25 insertions(+), 24 deletions(-) delete mode 100644 utils/common_utils/utils.h create mode 100644 utils/common_utils/utils_new.h diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index 3a8e4871..c4dd88e8 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -5,9 +5,10 @@ #include // user #include "mipi.h" -#include "utils.h" +//#include "utils.h" #include "user_api.h" #include "app_raw.h" +#include "utils_new.h" void user_app_raw(streaming_chanend_t c_cam_api){ @@ -24,6 +25,7 @@ void user_app_raw(streaming_chanend_t c_cam_api){ printf("Image captured...\n"); // Save the image to a file - write_image_raw("capture.bin", image_buffer); + //write_image_raw("capture.bin", image_buffer); + write_image_new("capture.bin", image_buffer, MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES); exit(0); } diff --git a/utils/common_utils/utils.c b/utils/common_utils/utils.c index 658db7fe..dd708454 100644 --- a/utils/common_utils/utils.c +++ b/utils/common_utils/utils.c @@ -1,8 +1,8 @@ -#include "utils.h" +#include "utils_new.h" #include -void write_image(char * filename, uint8_t * image, const size_t height, const size_t width) +void write_image_new(char * filename, uint8_t * image, const size_t height, const size_t width) { xscope_file_t fp = xscope_open_file(filename, "wb"); @@ -13,7 +13,7 @@ void write_image(char * filename, uint8_t * image, const size_t height, const si printf("Image dimentions: %d x %d\n", width, height); } -void not_silly_memcpy(void * dst, void * src, size_t size) +void c_memcpy(void * dst, void * src, size_t size) { memcpy(dst, src, size); } diff --git a/utils/common_utils/utils.h b/utils/common_utils/utils.h deleted file mode 100644 index 1a5990e4..00000000 --- a/utils/common_utils/utils.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#ifdef __XC__ -extern "C" { -#endif - -#include "xscope_io_device.h" - -void write_image(char * filename, uint8_t * image, const size_t height, const size_t width); -void not_silly_memcpy(void * dst, void * src, size_t size); - -#ifdef __XC__ -} -#endif diff --git a/utils/common_utils/utils_new.h b/utils/common_utils/utils_new.h new file mode 100644 index 00000000..095b25d5 --- /dev/null +++ b/utils/common_utils/utils_new.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +#ifdef __XC__ +extern "C" { +#endif + +#include "xscope_io_device.h" + +void write_image_new(char * filename, uint8_t * image, const size_t height, const size_t width); +void c_memcpy(void * dst, void * src, size_t size); + +#ifdef __XC__ +} +#endif From f4012025a02ac122b4844e03761746b25d3a3422 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Wed, 7 Jun 2023 13:36:26 +0100 Subject: [PATCH 040/306] adding xor from mipi shim, disabling xor from MipiPacketRx.s --- camera/api/camera.h | 2 +- camera/src/camera.xc | 8 ++++---- modules/mipi/api/mipi_defines.h | 25 +++++++++++++++++++++++++ modules/mipi/src/MipiPacketRx.S | 2 +- python/decode_downsampled.py | 14 +++----------- requirements.txt | 10 ---------- sensors/api/sensor.h | 13 +++++++++---- sensors/api/sensor_defs.h | 3 +++ 8 files changed, 46 insertions(+), 31 deletions(-) delete mode 100644 requirements.txt diff --git a/camera/api/camera.h b/camera/api/camera.h index ef9f0ec4..8895544f 100644 --- a/camera/api/camera.h +++ b/camera/api/camera.h @@ -29,7 +29,7 @@ * thread to store received packets. */ #define DEMUX_DATATYPE 0 // RESERVED -#define DEMUX_MODE 0x00 // no demux +#define DEMUX_MODE CONFIG_DEMUX_MODE // xor bias or not #define DEMUX_EN 0 // DISABLE DEMUX #define MIPI_CLK_DIV 1 // CLK DIVIDER #define MIPI_CFG_CLK_DIV 3 // CFG DIVIDER diff --git a/camera/src/camera.xc b/camera/src/camera.xc index fc54e0b5..5b4a346b 100644 --- a/camera/src/camera.xc +++ b/camera/src/camera.xc @@ -36,11 +36,11 @@ void camera_main( // chan c_sensor_control; // See AN for MIPI shim - // 0x7E42 >> 0111 1110 0100 0010 - // in the explorer BOARD DPDN is swap + // 0x7E42 >> 0 1 1 1 1 1 1 001 000 010 // + // Assigning lanes and polarities write_node_config_reg(mipi_tile, XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, - 0x7E42); //TODO decompose into different values + DEFAULT_MIPI_SHIM_CFG); // send packet to MIPI shim MipiPacketRx_init(mipi_tile, @@ -99,7 +99,7 @@ void camera_main_raw( // in the explorer BOARD DPDN is swap write_node_config_reg(mipi_tile, XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, - 0x7E42); //TODO decompose into different values + DEFAULT_MIPI_SHIM_CFG); //TODO decompose into different values // send packet to MIPI shim MipiPacketRx_init(mipi_tile, diff --git a/modules/mipi/api/mipi_defines.h b/modules/mipi/api/mipi_defines.h index a74ce7d4..7dc6bfaf 100644 --- a/modules/mipi/api/mipi_defines.h +++ b/modules/mipi/api/mipi_defines.h @@ -122,3 +122,28 @@ typedef enum xMIPI_DemuxMode_t { XMIPI_DEMUXMODE_12TO8 = 6, XMIPI_DEMUXMODE_14TO8 = 7 } xMIPI_DemuxMode_t; + + +// Mipi shim configuration +#define _ENABLE_LAN1 (1 << 14) +#define _ENABLE_LAN0 (1 << 13) +#define _ENABLE_CLK (1 << 12) +#define _DPDN_SWAP_LAN1 (1 << 11) +#define _DPDN_SWAP_LAN0 (1 << 10) +#define _DPDN_SWAP_CLK (1 << 9) + +#define _LANE_SWAP_LAN1_DEFAULT (1 << 6) +#define _LANE_SWAP_LAN0_DEFAULT (0 << 3) +#define _LANE_SWAP_CLK_DEFAULT (2 << 0) + +#define DEFAULT_MIPI_SHIM_CFG ( \ + _ENABLE_LAN1 |\ + _ENABLE_LAN0 |\ + _ENABLE_CLK |\ + _DPDN_SWAP_LAN1 |\ + _DPDN_SWAP_LAN0 |\ + _DPDN_SWAP_CLK |\ + _LANE_SWAP_LAN1_DEFAULT |\ + _LANE_SWAP_LAN0_DEFAULT |\ + _LANE_SWAP_CLK_DEFAULT \ +) diff --git a/modules/mipi/src/MipiPacketRx.S b/modules/mipi/src/MipiPacketRx.S index b3715d1e..a2c5617a 100644 --- a/modules/mipi/src/MipiPacketRx.S +++ b/modules/mipi/src/MipiPacketRx.S @@ -191,7 +191,7 @@ or r8, r8, r9 { in S0, res[P_RXD] ; add BUFF, BUFF, 4 } .L_RX_LONG_LOOP: // { byterev S0, S0 ; } - { xor S0, S0, r8 ; } + // { xor S0, S0, r8 ; } { add BUFF, BUFF, 4 ; stw S0, BUFF[0] } { in S0, res[P_RXD] ; bu .L_RX_LONG_LOOP } diff --git a/python/decode_downsampled.py b/python/decode_downsampled.py index aee12e4e..5fd8d144 100644 --- a/python/decode_downsampled.py +++ b/python/decode_downsampled.py @@ -1,15 +1,7 @@ """ -Info of RAW streams - -SBGGR8: - 640x480 : pixels - BGGR is the order of the Bayer pattern - -SBGGR10_CSI2P : - 640x480 stride 800 : bytes per line - SBGGR10_CSI2P : 10bits per pixel, CSI2 packed format - BGGR is the order of the Bayer pattern - few padding bytes on the end of every row to match bits +Downsampled image info: + * 160x120 + * R G B """ import cv2 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index f0f52b81..00000000 --- a/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -colorama==0.4.6 -docopt==0.6.2 -packaging==23.0 -pykwalify==1.8.0 -python-dateutil==2.8.2 -PyYAML==6.0 -ruamel.yaml==0.17.21 -ruamel.yaml.clib==0.2.7 -six==1.16.0 -west==1.0.0 diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index 9d62f6b7..3db9ee08 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -8,20 +8,25 @@ // -------------- Sensor abstraction layer. -------------- #include "sensor_defs.h" -// This is user defined +// Camera support #define CONFIG_IMX219_SUPPORT ENABLED #define CONFIG_GC2145_SUPPORT DISABLED -#define CROP_ENABLED DISABLED +// Crop selection +#define CROP_ENABLED DISABLED #define CONFIG_MODE MODE_VGA_640x480 -#define CONFIG_MIPI_FORMAT MIPI_DT_RAW8 +// Mipi format and mode +#define CONFIG_MIPI_FORMAT MIPI_DT_RAW8 +#define CONFIG_DEMUX_MODE BIAS_ENABLED #define MIPI_PKT_BUFFER_COUNT 4 // FPS settings #define FPS_13 // allowed values: [FPS_13, FPS_24, FPS_30, FPS_53, FPS_76] -// Inlcude custom libraries +// -------------------------------------------------------- + +// Include custom libraries #if CONFIG_IMX219_SUPPORT #include "imx219.h" #endif diff --git a/sensors/api/sensor_defs.h b/sensors/api/sensor_defs.h index d1608598..d732c0be 100644 --- a/sensors/api/sensor_defs.h +++ b/sensors/api/sensor_defs.h @@ -9,3 +9,6 @@ #define MIPI_DT_RAW8 0x2A #define MIPI_DT_RAW10 0x2B + +#define BIAS_DISABLED 0x00 // no demux +#define BIAS_ENABLED 0x80 // bias From 799f7e594d80b82567d10fff12778d1eb5ecf38e Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Wed, 7 Jun 2023 14:37:32 +0100 Subject: [PATCH 041/306] moving requirements.txt 7 adding extra time to camera raw --- examples/take_picture_raw/src/app_raw.c | 4 ++-- python/requirements.txt => requirements.txt | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename python/requirements.txt => requirements.txt (100%) diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index 3a8e4871..4d4a543c 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -15,8 +15,8 @@ void user_app_raw(streaming_chanend_t c_cam_api){ int8_t image_buffer[H_RAW*W_RAW]; memset(image_buffer, -128, H_RAW*W_RAW); - // wait for set the camera - delay_milliseconds(500); + // wait for the camera to set I2C parameters + delay_milliseconds(2500); // Request an image printf("Requesting image...\n"); diff --git a/python/requirements.txt b/requirements.txt similarity index 100% rename from python/requirements.txt rename to requirements.txt From 9b786f73da920639eb74e39d9c155851cdec0cc9 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Wed, 7 Jun 2023 14:39:17 +0100 Subject: [PATCH 042/306] adding missing lines to main.xc --- examples/take_picture_raw/CMakeLists.txt | 2 +- examples/take_picture_raw/src/app_raw.c | 1 + examples/take_picture_raw/src/main.xc | 6 ++++++ utils/common_utils/utils.c | 13 ++++++++++++- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/examples/take_picture_raw/CMakeLists.txt b/examples/take_picture_raw/CMakeLists.txt index 25ff05ce..5663654a 100644 --- a/examples/take_picture_raw/CMakeLists.txt +++ b/examples/take_picture_raw/CMakeLists.txt @@ -5,7 +5,7 @@ # <--- Set the executable set(TARGET example_take_picture) -file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.xc) +file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.*c) set(APP_INCLUDES ${CMAKE_CURRENT_LIST_DIR}/src ) diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index c4dd88e8..7178d889 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -25,6 +25,7 @@ void user_app_raw(streaming_chanend_t c_cam_api){ printf("Image captured...\n"); // Save the image to a file + printf("before writing mage\n"); //write_image_raw("capture.bin", image_buffer); write_image_new("capture.bin", image_buffer, MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES); exit(0); diff --git a/examples/take_picture_raw/src/main.xc b/examples/take_picture_raw/src/main.xc index 9ad619ed..ca3afa7a 100644 --- a/examples/take_picture_raw/src/main.xc +++ b/examples/take_picture_raw/src/main.xc @@ -12,6 +12,9 @@ on tile[0]: port p_scl = XS1_PORT_1N; on tile[0]: port p_sda = XS1_PORT_1O; +extern "C" { +#include "xscope_io_device.h" +} /** * Declaration of the MIPI interface ports: @@ -28,7 +31,9 @@ int main(void) { streaming chan c_cam_api; i2c_master_if i2c[1]; + chan xscope_chan; par { + xscope_host_data(xscope_chan); on tile[0]: i2c_master(i2c, 1, p_scl, p_sda, Kbps); on tile[MIPI_TILE]: camera_main_raw(tile[MIPI_TILE], @@ -40,6 +45,7 @@ int main(void) i2c[0], c_cam_api); + on tile[MIPI_TILE]: xscope_io_init(xscope_chan); on tile[MIPI_TILE]: user_app_raw(c_cam_api); } return 0; diff --git a/utils/common_utils/utils.c b/utils/common_utils/utils.c index dd708454..925384d0 100644 --- a/utils/common_utils/utils.c +++ b/utils/common_utils/utils.c @@ -4,9 +4,20 @@ void write_image_new(char * filename, uint8_t * image, const size_t height, const size_t width) { + printf("enetered funtion\n"); xscope_file_t fp = xscope_open_file(filename, "wb"); + printf("starting image write\n"); - xscope_fwrite(&fp, image, height * width * sizeof(uint8_t)); + for(size_t k = 0; k < height; k++) + { + for(size_t j = 0; j < width; j++) + { + size_t pos = k * width + j; + xscope_fwrite(&fp, &image[pos], sizeof(uint8_t)); + } + } + + //xscope_fwrite(&fp, image, height * width * sizeof(uint8_t)); xscope_close_all_files(); printf("Output file: %s\n", filename); From ce2bf2f8e45970013fef5011da698db0fc8a7cb4 Mon Sep 17 00:00:00 2001 From: Aaron Stewart Date: Wed, 7 Jun 2023 09:53:48 -0400 Subject: [PATCH 043/306] Modifies compilation flags to avoid unnecessary recompilation of a XC files. --- CMakeLists.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index b4c3cd34..58ff58b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,9 @@ project(fwk_camera) ## Enable languages for project enable_language(CXX C ASM) +## Fixes unnecessary recompilation of .xc files in Windows builds. +string(REPLACE "-MD" "-MMD" CMAKE_DEPFILE_FLAGS_C ${CMAKE_DEPFILE_FLAGS_C}) + if(${CMAKE_SYSTEM_PROCESSOR} STREQUAL XCORE_XS3A) ## Add frameworks From 3dcf31adb1c5db0df256ec7bae82620ec0e988fd Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Wed, 7 Jun 2023 15:08:10 +0100 Subject: [PATCH 044/306] adding xscope script --- examples/simple_timing/CMakeLists.txt | 14 ++-- .../take_picture_downsample/CMakeLists.txt | 14 ++-- examples/take_picture_raw/CMakeLists.txt | 14 ++-- python/run_xscope_bin.py | 76 +++++++++++++++++++ 4 files changed, 94 insertions(+), 24 deletions(-) create mode 100644 python/run_xscope_bin.py diff --git a/examples/simple_timing/CMakeLists.txt b/examples/simple_timing/CMakeLists.txt index 1f621633..9b1284f2 100644 --- a/examples/simple_timing/CMakeLists.txt +++ b/examples/simple_timing/CMakeLists.txt @@ -5,12 +5,6 @@ # <--- Set the executable set(TARGET example_timing) -file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.xc) -set(APP_INCLUDES - ${CMAKE_CURRENT_LIST_DIR}/src -) - - #********************** # Flags #********************** @@ -57,8 +51,12 @@ set(APP_COMMON_LINK_LIBRARIES # Tile Targets #********************** add_executable(${TARGET} EXCLUDE_FROM_ALL) -target_sources(${TARGET} PUBLIC ${APP_SOURCES}) -target_include_directories(${TARGET} PUBLIC ${APP_INCLUDES}) +target_sources(${TARGET} + PRIVATE + src/mipi_timing.xc + src/main.xc +) +target_include_directories(${TARGET} PUBLIC src) target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) diff --git a/examples/take_picture_downsample/CMakeLists.txt b/examples/take_picture_downsample/CMakeLists.txt index e6be537d..a9a78d24 100644 --- a/examples/take_picture_downsample/CMakeLists.txt +++ b/examples/take_picture_downsample/CMakeLists.txt @@ -5,12 +5,6 @@ # <--- Set the executable set(TARGET example_take_picture_downsample) -file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.*c) -set(APP_INCLUDES - ${CMAKE_CURRENT_LIST_DIR}/src -) - - #********************** # Flags #********************** @@ -57,8 +51,12 @@ set(APP_COMMON_LINK_LIBRARIES # Tile Targets #********************** add_executable(${TARGET} EXCLUDE_FROM_ALL) -target_sources(${TARGET} PUBLIC ${APP_SOURCES}) -target_include_directories(${TARGET} PUBLIC ${APP_INCLUDES}) +target_sources(${TARGET} + PRIVATE + src/app.c + src/main.xc +) +target_include_directories(${TARGET} PUBLIC src) target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) diff --git a/examples/take_picture_raw/CMakeLists.txt b/examples/take_picture_raw/CMakeLists.txt index e01bb581..77a471de 100644 --- a/examples/take_picture_raw/CMakeLists.txt +++ b/examples/take_picture_raw/CMakeLists.txt @@ -5,12 +5,6 @@ # <--- Set the executable set(TARGET example_take_picture) -file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.*c) -set(APP_INCLUDES - ${CMAKE_CURRENT_LIST_DIR}/src -) - - #********************** # Flags #********************** @@ -60,8 +54,12 @@ set(APP_COMMON_LINK_LIBRARIES # Tile Targets #********************** add_executable(${TARGET} EXCLUDE_FROM_ALL) -target_sources(${TARGET} PUBLIC ${APP_SOURCES}) -target_include_directories(${TARGET} PUBLIC ${APP_INCLUDES}) +target_sources(${TARGET} + PRIVATE + src/app_raw.c + src/main.xc +) +target_include_directories(${TARGET} PUBLIC src) target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) diff --git a/python/run_xscope_bin.py b/python/run_xscope_bin.py new file mode 100644 index 00000000..a8c9e76f --- /dev/null +++ b/python/run_xscope_bin.py @@ -0,0 +1,76 @@ +# Copyright 2022 XMOS LIMITED. +# This Software is subject to the terms of the XMOS Public Licence: Version 1. +import xscope_fileio +import argparse +import shutil +import subprocess + +def get_adapter_id(): + try: + xrun_out = subprocess.check_output(['xrun', '-l'], text=True, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + print('Error: %s' % e.output) + assert False + + xrun_out = xrun_out.split('\n') + # Check that the first 4 lines of xrun_out match the expected lines + expected_header = ["", "Available XMOS Devices", "----------------------", ""] + if len(xrun_out) < len(expected_header): + raise RuntimeError( + f"Error: xrun output:\n{xrun_out}\n" + f"does not contain expected header:\n{expected_header}" + ) + + header_match = True + for i, expected_line in enumerate(expected_header): + if xrun_out[i] != expected_line: + header_match = False + + if not header_match: + raise RuntimeError( + f"Error: xrun output header:\n{xrun_out[:4]}\n" + f"does not match expected header:\n{expected_header}" + ) + + try: + if "No Available Devices Found" in xrun_out[4]: + raise RuntimeError(f"Error: No available devices found\n") + return + except IndexError: + raise RuntimeError(f"Error: xrun output is too short:\n{xrun_out}\n") + + for line in xrun_out[6:]: + if line.strip(): + adapterID = line[26:34].strip() + status = line[34:].strip() + else: + continue + print("adapter_id = ",adapterID) + return adapterID + + +def parse_arguments(): + parser = argparse.ArgumentParser() + parser.add_argument("xe", nargs='?', + help=".xe file to run") + args = parser.parse_args() + return args + +def run(xe, return_stdout=False): + + adapter_id = get_adapter_id() + print("Running on adapter_id ",adapter_id) + if return_stdout == False: + xscope_fileio.run_on_target(adapter_id, xe) + else: + with open("prof.txt", "w+") as ff: + xscope_fileio.run_on_target(adapter_id, xe, stdout=ff) + ff.seek(0) + stdout = ff.readlines() + return stdout + +if __name__ == "__main__": + args = parse_arguments() + assert args.xe is not None, "Specify vaild .xe file" + + run(args.xe) From 741be236215772fb03508c22ddbc6408230c4f7c Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Wed, 7 Jun 2023 15:18:59 +0100 Subject: [PATCH 045/306] typo error, DEFAULT_MIPI_SHIM_CFG3 --- camera/src/camera.xc | 4 ++-- modules/mipi/api/mipi_defines.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/camera/src/camera.xc b/camera/src/camera.xc index 5b4a346b..105a4d19 100644 --- a/camera/src/camera.xc +++ b/camera/src/camera.xc @@ -40,7 +40,7 @@ void camera_main( // Assigning lanes and polarities write_node_config_reg(mipi_tile, XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, - DEFAULT_MIPI_SHIM_CFG); + DEFAULT_MIPI_SHIM_CFG3); // send packet to MIPI shim MipiPacketRx_init(mipi_tile, @@ -99,7 +99,7 @@ void camera_main_raw( // in the explorer BOARD DPDN is swap write_node_config_reg(mipi_tile, XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, - DEFAULT_MIPI_SHIM_CFG); //TODO decompose into different values + DEFAULT_MIPI_SHIM_CFG3); //TODO decompose into different values // send packet to MIPI shim MipiPacketRx_init(mipi_tile, diff --git a/modules/mipi/api/mipi_defines.h b/modules/mipi/api/mipi_defines.h index 7dc6bfaf..cb0b5481 100644 --- a/modules/mipi/api/mipi_defines.h +++ b/modules/mipi/api/mipi_defines.h @@ -136,7 +136,7 @@ typedef enum xMIPI_DemuxMode_t { #define _LANE_SWAP_LAN0_DEFAULT (0 << 3) #define _LANE_SWAP_CLK_DEFAULT (2 << 0) -#define DEFAULT_MIPI_SHIM_CFG ( \ +#define DEFAULT_MIPI_SHIM_CFG3 ( \ _ENABLE_LAN1 |\ _ENABLE_LAN0 |\ _ENABLE_CLK |\ From 396aec6bbcae65777c9c6091f874eab77c74cc75 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Wed, 7 Jun 2023 17:08:23 +0100 Subject: [PATCH 046/306] changing API, cmake cleanup --- .gitignore | 3 ++- examples/simple_timing/CMakeLists.txt | 2 +- examples/take_picture_raw/CMakeLists.txt | 2 +- examples/take_picture_raw/src/app_raw.c | 9 ++++----- launch_cmake.sh | 2 +- python/decode_raw8.py | 1 + utils/common_utils/utils.c | 18 ++++-------------- utils/common_utils/utils_new.h | 2 +- 8 files changed, 15 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 3e0a236d..16491c1d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ **/*.eggs/* **/.ipynb_checkpoints/* .venv +.python-version # Build & debug cruft **/.build*/* @@ -82,4 +83,4 @@ temp # dotenv .env -.png \ No newline at end of file +**/*.png \ No newline at end of file diff --git a/examples/simple_timing/CMakeLists.txt b/examples/simple_timing/CMakeLists.txt index 9b1284f2..f5731737 100644 --- a/examples/simple_timing/CMakeLists.txt +++ b/examples/simple_timing/CMakeLists.txt @@ -3,7 +3,7 @@ #********************** # <--- Set the executable -set(TARGET example_timing) +set(TARGET example_simple_timing) #********************** # Flags diff --git a/examples/take_picture_raw/CMakeLists.txt b/examples/take_picture_raw/CMakeLists.txt index 77a471de..6cf7d109 100644 --- a/examples/take_picture_raw/CMakeLists.txt +++ b/examples/take_picture_raw/CMakeLists.txt @@ -3,7 +3,7 @@ #********************** # <--- Set the executable -set(TARGET example_take_picture) +set(TARGET example_take_picture_raw) #********************** # Flags diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index 7178d889..37f5d333 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -13,8 +13,8 @@ void user_app_raw(streaming_chanend_t c_cam_api){ // set the input image to 0 - int8_t image_buffer[H_RAW*W_RAW]; - memset(image_buffer, -128, H_RAW*W_RAW); + int8_t image_buffer[H_RAW * W_RAW]; + memset(image_buffer, -128, H_RAW * W_RAW); // wait for set the camera delay_milliseconds(500); @@ -25,8 +25,7 @@ void user_app_raw(streaming_chanend_t c_cam_api){ printf("Image captured...\n"); // Save the image to a file - printf("before writing mage\n"); - //write_image_raw("capture.bin", image_buffer); - write_image_new("capture.bin", image_buffer, MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES); + size_t img_size = MIPI_IMAGE_HEIGHT_PIXELS * MIPI_LINE_WIDTH_BYTES * sizeof(uint8_t); + write_image_new("capture.bin", (uint8_t * ) &image_buffer[0], img_size, MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES); exit(0); } diff --git a/launch_cmake.sh b/launch_cmake.sh index ce6b0057..a211a9d9 100755 --- a/launch_cmake.sh +++ b/launch_cmake.sh @@ -1,7 +1,7 @@ #!/bin/bash # clean everything -sudo rm -r build/ +rm -r build/ # build again diff --git a/python/decode_raw8.py b/python/decode_raw8.py index 401b64e0..224e91ca 100644 --- a/python/decode_raw8.py +++ b/python/decode_raw8.py @@ -22,6 +22,7 @@ from utils import * input_name = os.getenv('BINARY_IMG_PATH') +#input_name = Path(__file__).parent / "capture.bin" width, height = 640, 480 diff --git a/utils/common_utils/utils.c b/utils/common_utils/utils.c index 925384d0..d224ea8b 100644 --- a/utils/common_utils/utils.c +++ b/utils/common_utils/utils.c @@ -2,25 +2,15 @@ #include "utils_new.h" #include -void write_image_new(char * filename, uint8_t * image, const size_t height, const size_t width) +void write_image_new(char * filename, uint8_t * image, const size_t size, const size_t height, const size_t width) { - printf("enetered funtion\n"); xscope_file_t fp = xscope_open_file(filename, "wb"); - printf("starting image write\n"); - for(size_t k = 0; k < height; k++) - { - for(size_t j = 0; j < width; j++) - { - size_t pos = k * width + j; - xscope_fwrite(&fp, &image[pos], sizeof(uint8_t)); - } - } - - //xscope_fwrite(&fp, image, height * width * sizeof(uint8_t)); + printf("Writing image...\n"); + xscope_fwrite(&fp, image, size); xscope_close_all_files(); - printf("Output file: %s\n", filename); + printf("Image written into file: %s\n", filename); printf("Image dimentions: %d x %d\n", width, height); } diff --git a/utils/common_utils/utils_new.h b/utils/common_utils/utils_new.h index 095b25d5..13b17569 100644 --- a/utils/common_utils/utils_new.h +++ b/utils/common_utils/utils_new.h @@ -10,7 +10,7 @@ extern "C" { #include "xscope_io_device.h" -void write_image_new(char * filename, uint8_t * image, const size_t height, const size_t width); +void write_image_new(char * filename, uint8_t * image, const size_t size, const size_t height, const size_t width); void c_memcpy(void * dst, void * src, size_t size); #ifdef __XC__ From c081b66b32dea40165614870740aaa0407442730 Mon Sep 17 00:00:00 2001 From: Aaron Stewart Date: Wed, 7 Jun 2023 12:19:29 -0400 Subject: [PATCH 047/306] Clean up --- camera/src/camera.xc | 24 ++++++++---- modules/mipi/api/mipi.h | 59 +++++++++++------------------ modules/mipi/api/mipi_defines.h | 39 ++++++++++++++++++- modules/mipi/src/MipiPacketRx.c | 65 -------------------------------- modules/mipi/src/MipiPacketRx.xc | 24 ++++++------ 5 files changed, 85 insertions(+), 126 deletions(-) delete mode 100644 modules/mipi/src/MipiPacketRx.c diff --git a/camera/src/camera.xc b/camera/src/camera.xc index 5b4a346b..e5177040 100644 --- a/camera/src/camera.xc +++ b/camera/src/camera.xc @@ -40,7 +40,13 @@ void camera_main( // Assigning lanes and polarities write_node_config_reg(mipi_tile, XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, - DEFAULT_MIPI_SHIM_CFG); + DEFAULT_MIPI_SHIM_CFG); + + unsigned mipi_shim_cfg0 = MIPI_SHIM_CFG0_PACK(DEMUX_EN, + DEMUX_DATATYPE, + DEMUX_MODE, + 0, + (DEMUX_MODE)); // send packet to MIPI shim MipiPacketRx_init(mipi_tile, @@ -49,11 +55,9 @@ void camera_main( p_mipi_rxa, p_mipi_clk, clk_mipi, - DEMUX_EN, - DEMUX_DATATYPE, - DEMUX_MODE, + mipi_shim_cfg0, MIPI_CLK_DIV, - MIPI_CFG_CLK_DIV); + MIPI_CFG_CLK_DIV); // Start camera and its configurations int r = 0; @@ -101,6 +105,12 @@ void camera_main_raw( XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, DEFAULT_MIPI_SHIM_CFG); //TODO decompose into different values + unsigned mipi_shim_cfg0 = MIPI_SHIM_CFG0_PACK(DEMUX_EN, + DEMUX_DATATYPE, + DEMUX_MODE, + 0, + (DEMUX_MODE)); + // send packet to MIPI shim MipiPacketRx_init(mipi_tile, p_mipi_rxd, @@ -108,9 +118,7 @@ void camera_main_raw( p_mipi_rxa, p_mipi_clk, clk_mipi, - DEMUX_EN, - DEMUX_DATATYPE, - DEMUX_MODE, + mipi_shim_cfg0, MIPI_CLK_DIV, MIPI_CFG_CLK_DIV); diff --git a/modules/mipi/api/mipi.h b/modules/mipi/api/mipi.h index 3466e021..64d5b3e0 100644 --- a/modules/mipi/api/mipi.h +++ b/modules/mipi/api/mipi.h @@ -4,54 +4,37 @@ #include #include +#include "xccompat.h" + #include "mipi_defines.h" -#ifndef __XC__ +#ifdef __XC__ +typedef tileref tileref_t; +typedef clock xclock_t; +typedef in port in_port_t; +#else #include #include #include +typedef unsigned tileref_t; +typedef port_t in_buffered_port_32_t; +typedef port_t in_port_t; #endif -#ifdef __XC__ - -void MipiPacketRx_init( - tileref tile, - buffered in port:32 p_mipi_rxd, - in port p_mipi_rxv, - in port p_mipi_rxa, - in port p_mipi_clk, - clock clk_mipi, - uint32_t demuxEn, - uint32_t dataType, - xMIPI_DemuxMode_t demuxMode, - uint32_t mipiClkDiv, - uint32_t cfgClkDiv); - -void MipiPacketRx( - buffered in port:32 p_mipi_rxd, - in port p_mipi_rxa, - streaming chanend c_pkt, - streaming chanend c_ctrl); - -#else - void MipiPacketRx_init( - unsigned tile, - port_t p_mipi_rxd, - port_t p_mipi_rxv, - port_t p_mipi_rxa, - port_t p_mipi_clk, + tileref_t tile, + in_buffered_port_32_t p_mipi_rxd, + in_port_t p_mipi_rxv, + in_port_t p_mipi_rxa, + in_port_t p_mipi_clk, xclock_t clk_mipi, - uint32_t demuxEn, - uint32_t dataType, - xMIPI_DemuxMode_t demuxMode, + unsigned mipi_shim_cfg0, uint32_t mipiClkDiv, uint32_t cfgClkDiv); void MipiPacketRx( - port_t p_mipi_rxd, - port_t p_mipi_rxa, - chanend_t c_pkt, - chanend_t c_ctrl); - -#endif + in_buffered_port_32_t p_mipi_rxd, + in_port_t p_mipi_rxa, + streaming_chanend_t c_pkt, + streaming_chanend_t c_ctrl); + \ No newline at end of file diff --git a/modules/mipi/api/mipi_defines.h b/modules/mipi/api/mipi_defines.h index 7dc6bfaf..97321f6b 100644 --- a/modules/mipi/api/mipi_defines.h +++ b/modules/mipi/api/mipi_defines.h @@ -13,6 +13,8 @@ typedef unsigned mipi_header_t; #define MIPI_GET_WORD_COUNT(HEADER) ( ((HEADER) >> 8) & 0xFFFF ) + + /** * 0x00 to 0x07 - Synchronization Short Packet Data Types * 0x08 to 0x0F - Generic Short Packet Data Types @@ -119,10 +121,43 @@ typedef enum xMIPI_DemuxMode_t { XMIPI_DEMUXMODE_14TO16 = 3, XMIPI_DEMUXMODE_565TO88 = 4, XMIPI_DEMUXMODE_8TO10 = 5, - XMIPI_DEMUXMODE_12TO8 = 6, - XMIPI_DEMUXMODE_14TO8 = 7 + XMIPI_DEMUXMODE_12TO8 = 6, // These are not in the shim documentation + XMIPI_DEMUXMODE_14TO8 = 7 // These are not in the shim documentation } xMIPI_DemuxMode_t; +/* + MIPI DPHY configuration register layout (MIPI_DPHY_CFG3) + + Bits Name Meaning + 14 _ENABLE_LAN1 Set to 0 to disable lane 1 receiver + 13 _ENABLE_LAN0 Set to 0 to disable lane 0 receiver + 12 _ENABLE_CLK Set to 0 to disable clock receiver + 11 _DPDN_SWAP_LAN1 Set to 1 to swap lane 1 polarity + 10 _DPDN_SWAP_LAN0 Set to 1 to swap lane 0 polarity + 9 _DPDN_SWAP_CLK Set to 1 to swap clock polarity + 8:6 _LANE_SWAP_LAN1 The pin over which to input lane 1 + 5:3 _LANE_SWAP_LAN0 The pin over which to input lane 0 + 2:0 _LANE_SWAP_CLK The pin over which to input clock +*/ + +/* + MIPI Shim configuration register layout (MIPI_SHIM_CFG0) + + Bits Name Meaning + 23 _BIAS Bias output pixels for VPU usage + 22 _DEMUX_STUFF Add zero byte after every RGB pixel + 21:16 _PIXEL_DEMUX_MODE Set demuxing mode + 15:8 _PIXEL_DEMUX_DATATYPE CSI-2 packet type to demux + 0 _PIXEL_DEMUX_EN Enable pixel demuxing +*/ +#define MIPI_SHIM_CFG0_PACK(DMUX_EN, DMUX_DT, DMUX_MODE, DMUX_STFF, DMUX_BIAS) \ + ( (((DMUX_EN) & 0x1 ) << 0) \ + | (((DMUX_DT) & 0xFF ) << 8) \ + | (((DMUX_MODE) & 0x3F ) << 16) \ + | (((DMUX_STFF) & 0x1 ) << 22) \ + | (((DMUX_BIAS) & 0x1 ) << 23)) + + // Mipi shim configuration #define _ENABLE_LAN1 (1 << 14) diff --git a/modules/mipi/src/MipiPacketRx.c b/modules/mipi/src/MipiPacketRx.c deleted file mode 100644 index 793c7dea..00000000 --- a/modules/mipi/src/MipiPacketRx.c +++ /dev/null @@ -1,65 +0,0 @@ - - -#include -#include -#include - -#include "mipi.h" - -#define SHIFT 0x3 -#define DELAY_SIZE 0x9 -#define DELAY_MASK (((1 << DELAY_SIZE) - 1) << SHIFT) -#define DELAY(x) (((x) & DELAY_MASK) >> SHIFT) -#define DELAY_SET(x, v) (((x) & ~DELAY_MASK) | (((v) << SHIFT) & DELAY_MASK)) - -void MipiPacketRx_init( - unsigned tile, - port_t p_mipi_rxd, - port_t p_mipi_rxv, - port_t p_mipi_rxa, - port_t p_mipi_clk, - xclock_t clk_mipi, - uint32_t demuxEn, - uint32_t dataType, - xMIPI_DemuxMode_t demuxMode, - uint32_t mipiClkDiv, - uint32_t cfgClkDiv) -{ - //configure_in_port_strobed_slave(p_mipi_rxd, p_mipi_rxv, clk_mipi); - port_protocol_in_strobed_slave(p_mipi_rxd, p_mipi_rxv, clk_mipi); - - //set_clock_src(clk_mipi, p_mipi_clk); - clock_set_ready_src(clk_mipi, p_mipi_clk); - - /* Sample on falling edge - shim outputting on rising */ - //set_clock_rise_delay(clk_mipi, 1); - __xcore_resource_setc(clk_mipi, DELAY_SET(0x9007, 1)); - - //set_pad_delay(p_mipi_rxa, 1); - __xcore_resource_setc(p_mipi_rxa, DELAY_SET(0x7007, 1)); - - //start_clock(clk_mipi); - clock_start(clk_mipi); - - // take DPHY out of reset - - //write_node_config_reg(tile, XS1_SSWITCH_MIPI_DPHY_CFG0_NUM, 3); - write_sswitch_reg(tile, XS1_SSWITCH_MIPI_DPHY_CFG0_NUM, 3); - - // set clock dividers to create suitable frequency - - //write_node_config_reg(tile, XS1_SSWITCH_MIPI_CLK_DIVIDER_NUM, mipiClkDiv); - write_sswitch_reg(tile, XS1_SSWITCH_MIPI_CLK_DIVIDER_NUM, mipiClkDiv); - - //write_node_config_reg(tile, XS1_SSWITCH_MIPI_CFG_CLK_DIVIDER_NUM, cfgClkDiv); - write_sswitch_reg(tile, XS1_SSWITCH_MIPI_CFG_CLK_DIVIDER_NUM, cfgClkDiv); - - /* Enable pixel demux */ - unsigned shimCfg = (((demuxMode & 0xff) << 16) | ((dataType & 0xff) << 8) | demuxEn); - //write_node_config_reg(tile, XS1_SSWITCH_MIPI_SHIM_CFG0_NUM, shimCfg); - write_sswitch_reg(tile, XS1_SSWITCH_MIPI_SHIM_CFG0_NUM, shimCfg); - - // Connect the xcore-ports to the MIPI demuxer/PHY. - int val = getps(XS1_PS_XCORE_CTRL0); - setps(XS1_PS_XCORE_CTRL0, val | 0x100); -} diff --git a/modules/mipi/src/MipiPacketRx.xc b/modules/mipi/src/MipiPacketRx.xc index b934d5fd..d7931a51 100644 --- a/modules/mipi/src/MipiPacketRx.xc +++ b/modules/mipi/src/MipiPacketRx.xc @@ -9,17 +9,15 @@ #include "mipi.h" void MipiPacketRx_init( - tileref tile, - buffered in port:32 p_mipi_rxd, - in port p_mipi_rxv, - in port p_mipi_rxa, - in port p_mipi_clk, - clock clk_mipi, - uint32_t demuxEn, - uint32_t dataType, - xMIPI_DemuxMode_t demuxMode, + tileref_t tile, + in_buffered_port_32_t p_mipi_rxd, + in_port_t p_mipi_rxv, + in_port_t p_mipi_rxa, + in_port_t p_mipi_clk, + xclock_t clk_mipi, + unsigned mipi_shim_cfg0, uint32_t mipiClkDiv, - uint32_t cfgClkDiv) + uint32_t cfgClkDiv) { configure_in_port_strobed_slave(p_mipi_rxd, p_mipi_rxv, clk_mipi); set_clock_src(clk_mipi, p_mipi_clk); @@ -36,9 +34,9 @@ void MipiPacketRx_init( XS1_SSWITCH_MIPI_CLK_DIVIDER_NUM, mipiClkDiv); write_node_config_reg(tile, XS1_SSWITCH_MIPI_CFG_CLK_DIVIDER_NUM, cfgClkDiv); - /* Enable pixel demux */ - unsigned shimCfg = (((demuxMode & 0xff) << 16) | ((dataType & 0xff) << 8) | demuxEn); - write_node_config_reg(tile, XS1_SSWITCH_MIPI_SHIM_CFG0_NUM, shimCfg); + + // set shim config register + write_node_config_reg(tile, XS1_SSWITCH_MIPI_SHIM_CFG0_NUM, mipi_shim_cfg0); // Connect the xcore-ports to the MIPI demuxer/PHY. int val = getps(XS1_PS_XCORE_CTRL0); From 4c2fdc1f7bc2f0ac66e789546befb869989f65be Mon Sep 17 00:00:00 2001 From: Aaron Stewart Date: Wed, 7 Jun 2023 17:08:02 -0400 Subject: [PATCH 048/306] Adds some unit tests for horizontal filtering. --- .gitignore | 1 - camera/api/camera.h | 1 + camera/api/image_hfilter.h | 50 ++- camera/api/isp.h | 10 + camera/src/asm/pixel_hfilter.S | 55 ++- camera/src/image_hfilter.c | 127 ++----- camera/src/packet_handler.c | 27 +- tests/unit_tests/CMakeLists.txt | 1 + tests/unit_tests/src/main.c | 24 ++ .../unit_tests/src/test/image_hfilter_test.c | 351 ++++++++++++++++++ tests/unit_tests/src/test/isp_test.c | 109 ++++++ tests/unit_tests/src/test_isp.c | 62 ---- tests/unit_tests/src/test_isp.h | 34 -- tests/unit_tests/src/unity_config.h | 10 + tests/unity.cmake | 21 +- 15 files changed, 636 insertions(+), 247 deletions(-) create mode 100644 tests/unit_tests/src/main.c create mode 100644 tests/unit_tests/src/test/image_hfilter_test.c create mode 100644 tests/unit_tests/src/test/isp_test.c delete mode 100644 tests/unit_tests/src/test_isp.c delete mode 100644 tests/unit_tests/src/test_isp.h create mode 100644 tests/unit_tests/src/unity_config.h diff --git a/.gitignore b/.gitignore index 3e0a236d..85c5fcb9 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,6 @@ *.dylib *.xe *.vcd -*.s *.xi *.i *.xmt diff --git a/camera/api/camera.h b/camera/api/camera.h index 8895544f..c672d184 100644 --- a/camera/api/camera.h +++ b/camera/api/camera.h @@ -22,6 +22,7 @@ #include "statistics.h" #include "user_api.h" #include "utils.h" +#include "isp.h" /** diff --git a/camera/api/image_hfilter.h b/camera/api/image_hfilter.h index ab60697a..3179d1eb 100644 --- a/camera/api/image_hfilter.h +++ b/camera/api/image_hfilter.h @@ -10,42 +10,40 @@ extern "C" { #endif +#define COEF_B0 (0.58254019f) +#define COEF_B1 (0.20872991f) + + void pixel_hfilter( int8_t output[], const int8_t input[], const int8_t coef[32], - const int16_t acc_hi[16], - const int16_t acc_lo[16], - const int16_t acc_shr[16], + const int32_t acc_init, + const unsigned shift, const int32_t input_stride, const unsigned output_count); -void image_hfilter( - int8_t pix_out[APP_IMAGE_WIDTH_PIXELS], - const int8_t pix_in[SENSOR_RAW_IMAGE_WIDTH_PIXELS], - const unsigned channel_index); - - +typedef struct { + int32_t acc_init; + int8_t coef[32]; + unsigned shift; +} hfilter_state_t; -extern -const int8_t hfilter_coef_bayered_even[32]; -extern -int8_t hfilter_red[32]; -extern -int8_t hfilter_green[32]; -extern -int8_t hfilter_blue[32]; - -extern -const int8_t hfilter_coef_bayered_odd[32]; - -extern -const int16_t hfilter_acc_init[2][16]; - -extern -const int16_t hfilter_shift[16]; +void image_hfilter_update_scale( + hfilter_state_t* state, + const float gain, + const unsigned offset); +// astew: I'm confused about this function now...this is in the camera +// code, but it's referencing macros that only make sense in the +// context of an application, because applications should be able to +// have different APP_IMAGE_WIDTH_PIXELS settings.. +void image_hfilter( + int8_t pix_out[APP_IMAGE_WIDTH_PIXELS], + const hfilter_state_t* state, + const int8_t pix_in[SENSOR_RAW_IMAGE_WIDTH_PIXELS]); + #if defined(__XC__) || defined(__cplusplus) } diff --git a/camera/api/isp.h b/camera/api/isp.h index 6b608022..076c124e 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -44,4 +44,14 @@ void isp_bilinear_resize( void isp_rotate_image(const uint8_t *src, uint8_t *dest, int width, int height); +int yuv_to_rgb( + int y, + int u, + int v); + +int rgb_to_yuv( + int r, + int g, + int b); + #endif // ISP_H diff --git a/camera/src/asm/pixel_hfilter.S b/camera/src/asm/pixel_hfilter.S index 1b7d367f..72544f40 100644 --- a/camera/src/asm/pixel_hfilter.S +++ b/camera/src/asm/pixel_hfilter.S @@ -5,7 +5,7 @@ .issue_mode dual #define FUNCTION_NAME pixel_hfilter -#define NSTACKWORDS 8 +#define NSTACKWORDS 36 .globl FUNCTION_NAME.nstackwords .globl FUNCTION_NAME.maxthreads @@ -30,9 +30,8 @@ void pixel_hfilter( int8_t output[], const int8_t input[], const int8_t coef[32], - const int16_t acc_hi[16], - const int16_t acc_lo[16], - const int16_t acc_shr[16], + const int32_t acc_init, + const unsigned shift, const int32_t input_stride, const unsigned output_count); @@ -40,17 +39,20 @@ void pixel_hfilter( **************************************************** */ -#define STK_ACC_LO (NSTACKWORDS+1) -#define STK_OUT_SHR (NSTACKWORDS+2) -#define STK_IN_STR (NSTACKWORDS+3) -#define STK_OUT_LEN (NSTACKWORDS+4) +#define STK_SHIFT (NSTACKWORDS+1) +#define STK_IN_STR (NSTACKWORDS+2) +#define STK_OUT_LEN (NSTACKWORDS+3) + +#define STK_VEC_ACC_HI (NSTACKWORDS-8) +#define STK_VEC_ACC_LO (NSTACKWORDS-16) +#define STK_VEC_SHIFT (NSTACKWORDS-24) #define output r0 #define input r1 #define coef r2 -#define acc_hi r3 -// #define acc_lo r4 -#define out_shr r5 +#define acc_init r3 + +#define shift r5 #define in_str r6 #define len r7 #define _16 r8 @@ -65,23 +67,44 @@ FUNCTION_NAME: std r6, r7, sp[2] std r8, r9, sp[3] +// First, broadcast the acc_init and shift values to the vector registers +{ mov r11, acc_init ; ldaw r4, sp[STK_VEC_ACC_HI] } + zip r11, acc_init, 4 + std r11, r11, r4[0] + std r11, r11, r4[1] + std r11, r11, r4[2] + std r11, r11, r4[3] +{ ldaw r4, sp[STK_VEC_ACC_LO] ; ldw r11, sp[STK_SHIFT] } + std acc_init, acc_init, r4[0] + std acc_init, acc_init, r4[1] + std acc_init, acc_init, r4[2] + std acc_init, acc_init, r4[3] +{ shl acc_init, r11, 16 ; ldaw r4, sp[STK_VEC_SHIFT] } +{ or acc_init, acc_init, r11 ; } + std acc_init, acc_init, r4[0] + std acc_init, acc_init, r4[1] + std acc_init, acc_init, r4[2] + std acc_init, acc_init, r4[3] + + ldc r11, 0x200 { ldc _16, 16 ; vsetc r11 } { mkmsk mask, 4 ; ldw len, sp[STK_OUT_LEN] } -{ and mask, len, mask ; ldw r11, sp[STK_ACC_LO] } -// if len isn't a multiple of 16 we're gonna do a bad thing. Fix this later. -{ ecallt mask ; ldw out_shr, sp[STK_OUT_SHR]} +{ and mask, len, mask ; ldaw r11, sp[STK_VEC_ACC_LO]} +// if len isn't a multiple of 16 we're gonna do a bad thing. +{ ecallt mask ; ldaw shift, sp[STK_VEC_SHIFT]} { mkmsk mask, 16 ; ldw in_str, sp[STK_IN_STR] } // Start at the end so we can proceed monotonically // input <-- input + (len*in_str) // output <-- output + (len) { add output, output, len ; vldc coef[0] } maccu coef, input, len, in_str +ldaw acc_init, sp[STK_VEC_ACC_HI] // We subtract at the beginning of the loop so it should work out correctly. .L_loop_top: - { sub input, input, in_str ; vldd acc_hi[0] } + { sub input, input, in_str ; vldd acc_init[0] } { sub output, output, _16 ; vldr r11[0] } { sub input, input, in_str ; vlmaccr input[0] } { sub input, input, in_str ; vlmaccr input[0] } @@ -99,7 +122,7 @@ maccu coef, input, len, in_str { sub input, input, in_str ; vlmaccr input[0] } { sub input, input, in_str ; vlmaccr input[0] } { ; vlmaccr input[0] } - { sub len, len, _16 ; vlsat out_shr[0] } + { sub len, len, _16 ; vlsat shift[0] } vstrpv output[0], mask { ; bt len, .L_loop_top } diff --git a/camera/src/image_hfilter.c b/camera/src/image_hfilter.c index b0cff939..6f5a6c35 100644 --- a/camera/src/image_hfilter.c +++ b/camera/src/image_hfilter.c @@ -23,104 +23,53 @@ //Note: for filter coefficients reference : python/filters.txt -#define A (0x1B) -#define B (0x4B) - -#define C (0x1B) -#define D (0x4B) - -/* -const int8_t hfilter_coef_bayered_even[32] = { - 0x1B,0x00,0x4B,0x00,0x1B,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, -}; - -const int8_t hfilter_coef_bayered_odd[32] = { - 0x00,0x1B,0x00,0x4B,0x00,0x1B,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, -}; -*/ - -#define AWB_gain_RED 1.3 -#define AWB_gain_BLUE 0.8 -#define AWB_gain_GREEN 1.3 - -int8_t hfilter_red[32] = { - C,0x00,D,0x00,C,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, -}; - -int8_t hfilter_green[32] = { - 0x00,A,0x00,B,0x00,A,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, -}; - -int8_t hfilter_blue[32] = { - 0x00,C,0x00,D,0x00,C,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, -}; - - -/// ---------------------------------------------------------------- -const int16_t MIN = 0x0000; -const int16_t MIN2 = 0x0000; - -const int16_t hfilter_acc_init[2][16] = { - { MIN,MIN,MIN,MIN,MIN,MIN,MIN,MIN, - MIN,MIN,MIN,MIN,MIN,MIN,MIN,MIN, }, - { MIN2,MIN2,MIN2,MIN2,MIN2,MIN2,MIN2,MIN2, - MIN2,MIN2,MIN2,MIN2,MIN2,MIN2,MIN2,MIN2, }, -}; - -const int16_t N = 7; -const int16_t hfilter_shift[16] = {N,N,N,N,N,N,N,N,N,N,N,N,N,N,N,N}; - -void apply_gains( - int8_t gains[3], - int8_t *filter, - const int8_t offset) + + +void image_hfilter_update_scale( + hfilter_state_t* state, + const float gain, + const unsigned offset) { - filter[0+offset] = gains[0]; - filter[2+offset] = gains[1]; - filter[4+offset] = gains[2]; + float sc_b0 = COEF_B0 * gain; + float sc_b1 = COEF_B1 * gain; + + // Faster than computing ceil(log2(__)) + if(sc_b0 <= 0.25f) state->shift = 9; + else if(sc_b0 <= 0.5f) state->shift = 8; + else if(sc_b0 <= 1.0f) state->shift = 7; + else if(sc_b0 <= 2.0f) state->shift = 6; + else state->shift = 5; + + const int shift_scale = 1 << state->shift; + + const float b0 = (sc_b0 * shift_scale); + const float b1 = (sc_b1 * shift_scale); + + const int8_t b0_s8 = (b0 >= INT8_MAX) ? INT8_MAX : (b0 + 0.5f); + const int8_t b1_s8 = (b1 >= INT8_MAX) ? INT8_MAX : (b1 + 0.5f); + + const unsigned s = offset; + + state->coef[0+s] = state->coef[4+s] = b1_s8; + state->coef[2+s] = b0_s8; + + const float sum_b = b0 + 2*b1; + + state->acc_init = (128 * (sum_b - shift_scale)); } + void image_hfilter( int8_t pix_out[APP_IMAGE_WIDTH_PIXELS], - const int8_t pix_in[SENSOR_RAW_IMAGE_WIDTH_PIXELS], - const unsigned channel_index) + const hfilter_state_t* state, + const int8_t pix_in[SENSOR_RAW_IMAGE_WIDTH_PIXELS]) { - // appply gains - // apply_gains({0x1B, 0x4B, 0x1B}, &hfilter_red[0], 0); - // apply_gains({0x1B, 0x4B, 0x1B}, &hfilter_green[0], 0); - // apply_gains({0x1B, 0x4B, 0x1B}, &hfilter_blue[0], 0); - - // group filters - int8_t* channel_hfilter_coefs_rggb[3] = { - &hfilter_red[0], - &hfilter_green[0], - &hfilter_blue[0], - }; - - assert(channel_index >= 0 && channel_index <= 2); - pixel_hfilter(&pix_out[0], &pix_in[0], - channel_hfilter_coefs_rggb[channel_index], - &hfilter_acc_init[0][0], - &hfilter_acc_init[1][0], - hfilter_shift, + &state->coef[0], + state->acc_init, + state->shift, HFILTER_INPUT_STRIDE, APP_IMAGE_WIDTH_PIXELS); } diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index 86c791ac..fdce8843 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -32,6 +32,19 @@ static struct { .out_line_number = 0, }; +hfilter_state_t hfilter_state[APP_IMAGE_CHANNEL_COUNT]; + +// Initial channel scales +#define AWB_gain_RED 1.3 +#define AWB_gain_BLUE 0.8 +#define AWB_gain_GREEN 1.3 + +float channel_scales[APP_IMAGE_CHANNEL_COUNT] = { + AWB_gain_RED, + AWB_gain_GREEN, + AWB_gain_BLUE +}; + static void handle_frame_start( @@ -39,6 +52,8 @@ void handle_frame_start( { // New frame is starting, reset the vertical filter accumulator states. for(int c = 0; c < APP_IMAGE_CHANNEL_COUNT; c++){ + image_hfilter_update_scale(&hfilter_state[c], channel_scales[c], + (c == 0)? 0 : 1); image_vfilter_frame_init(&vfilter_accs[c][0]); } @@ -80,8 +95,8 @@ unsigned handle_pixel_data( if(pattern == 0){ // Packet contains RGRGRGRGRGRGRGRGRG... ////// RED image_hfilter(&hfilt_row[0], - (int8_t*) &pkt->payload[0], - CHAN_RED); + &hfilter_state[CHAN_RED], + (int8_t*) &pkt->payload[0]); image_vfilter_process_row(&output_buffer[CHAN_RED][0], &vfilter_accs[CHAN_RED][0], @@ -89,8 +104,8 @@ unsigned handle_pixel_data( ////// GREEN image_hfilter(&hfilt_row[0], - (int8_t*) &pkt->payload[0], - CHAN_GREEN); + &hfilter_state[CHAN_GREEN], + (int8_t*) &pkt->payload[0]); // we now it is not the las row [2] image_vfilter_process_row(&output_buffer[CHAN_GREEN][0], @@ -101,8 +116,8 @@ unsigned handle_pixel_data( else { // Packet contains GBGBGBGBGBGBGBGBGBGB... ////// BLUE image_hfilter(&hfilt_row[0], - (int8_t*) &pkt->payload[0], - CHAN_BLUE); + &hfilter_state[CHAN_BLUE], + (int8_t*) &pkt->payload[0]); unsigned new_row = image_vfilter_process_row( &output_buffer[CHAN_BLUE][0], diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index 1a8e7499..cc76595d 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -16,6 +16,7 @@ set(APP_COMMON_LINK_LIBRARIES set(APP_LINK_OPTIONS "-report" "-target=${XCORE_TARGET}" + -fcmdline-buffer-bytes=1024 ${CMAKE_CURRENT_LIST_DIR}/config.xscope ) diff --git a/tests/unit_tests/src/main.c b/tests/unit_tests/src/main.c new file mode 100644 index 00000000..96ad4c16 --- /dev/null +++ b/tests/unit_tests/src/main.c @@ -0,0 +1,24 @@ +// Copyright 2020-2022 XMOS LIMITED. +// This Software is subject to the terms of the XMOS Public Licence: Version 1. + + +#include +#include +#include "unity_fixture.h" + +int main( + int argc, + const char* argv[]) +{ + xscope_config_io(XSCOPE_IO_BASIC); + + UnityGetCommandLineOptions(argc, argv); + UnityBegin(argv[0]); + + printf("\n"); + + RUN_TEST_GROUP(image_hfilter); + RUN_TEST_GROUP(isp_tests); + + return UNITY_END(); +} \ No newline at end of file diff --git a/tests/unit_tests/src/test/image_hfilter_test.c b/tests/unit_tests/src/test/image_hfilter_test.c new file mode 100644 index 00000000..8e5cfcbb --- /dev/null +++ b/tests/unit_tests/src/test/image_hfilter_test.c @@ -0,0 +1,351 @@ +// Copyright 2020-2022 XMOS LIMITED. +// This Software is subject to the terms of the XMOS Public Licence: Version 1. + +#include +#include +#include +#include +#include +#include + +#include "unity_fixture.h" + +#include "camera.h" + +TEST_GROUP_RUNNER(image_hfilter) { + RUN_TEST_CASE(image_hfilter, pixel_hfilter__basic); + RUN_TEST_CASE(image_hfilter, pixel_hfilter__acc_init); + RUN_TEST_CASE(image_hfilter, pixel_hfilter__apply_shift); + RUN_TEST_CASE(image_hfilter, pixel_hfilter__input_stride); + RUN_TEST_CASE(image_hfilter, pixel_hfilter__out_count); + RUN_TEST_CASE(image_hfilter, pixel_hfilter__alt_coef); + + RUN_TEST_CASE(image_hfilter, image_hfilter_update_scale__case1); + RUN_TEST_CASE(image_hfilter, image_hfilter_update_scale__case2); + RUN_TEST_CASE(image_hfilter, image_hfilter_update_scale__case3); + RUN_TEST_CASE(image_hfilter, image_hfilter_update_scale__case4); + + // RUN_TEST_CASE(image_hfilter, image_hfilter__case1); +} + +TEST_GROUP(image_hfilter); +TEST_SETUP(image_hfilter) { fflush(stdout); } +TEST_TEAR_DOWN(image_hfilter) {} + + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(image_hfilter, pixel_hfilter__basic) +{ + static const int8_t coef_count = 32; + static const unsigned output_count = 16; + static const int32_t input_stride = 4; + static const unsigned input_count = 32 + (output_count-1)*input_stride; + + int8_t coef[coef_count] = {0}; + coef[0] = 1; + + int8_t input[input_count] = {0}; + int8_t output[output_count] = {0}; + + for(int k = 0; k < input_count; k++) + input[k] = k>>2; + + const int32_t acc_init = 0; + const unsigned shift = 0; + + pixel_hfilter(output, input, coef, + acc_init, shift, input_stride, output_count); + + for(int k = 0; k < output_count; k++) + TEST_ASSERT_EQUAL_INT8(k, output[k]); +} + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(image_hfilter, pixel_hfilter__acc_init) +{ + static const int8_t coef_count = 32; + static const unsigned output_count = 16; + static const int32_t input_stride = 4; + static const unsigned input_count = 32 + (output_count-1)*input_stride; + + int8_t coef[coef_count] = {0}; + coef[0] = 1; + + int8_t input[input_count] = {0}; + int8_t output[output_count] = {0}; + + for(int k = 0; k < input_count; k++) + input[k] = k>>2; + + const int32_t acc_init = 10; + const unsigned shift = 0; + + pixel_hfilter(output, input, coef, + acc_init, shift, input_stride, output_count); + + + for(int k = 0; k < output_count; k++) + TEST_ASSERT_EQUAL_INT8(acc_init+k, output[k]); +} + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(image_hfilter, pixel_hfilter__apply_shift) +{ + static const int8_t coef_count = 32; + static const unsigned output_count = 16; + static const int32_t input_stride = 4; + static const unsigned input_count = 32 + (output_count-1)*input_stride; + + int8_t coef[coef_count] = {0}; + coef[0] = 1; + + int8_t input[input_count] = {0}; + int8_t output[output_count] = {0}; + + for(int k = 0; k < input_count; k++) + input[k] = k; + + const int32_t acc_init = 32; + const unsigned shift = 2; + + pixel_hfilter(output, input, coef, + acc_init, shift, input_stride, output_count); + + for(int k = 0; k < output_count; k++) + TEST_ASSERT_EQUAL_INT8( (acc_init>>2)+k, + output[k] ); +} + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(image_hfilter, pixel_hfilter__input_stride) +{ + static const int8_t coef_count = 32; + static const unsigned output_count = 16; + static const int32_t input_stride = 8; + static const unsigned input_count = 32 + (output_count-1)*input_stride; + + int8_t coef[coef_count] = {0}; + coef[0] = 1; + + int8_t input[input_count] = {0}; + int8_t output[output_count] = {0}; + + for(int k = 0; k < input_count; k++) + input[k] = k>>2; + + const int32_t acc_init = 10; + const unsigned shift = 0; + + pixel_hfilter(output, input, coef, + acc_init, shift, input_stride, output_count); + + for(int k = 0; k < output_count; k++) + TEST_ASSERT_EQUAL_INT8(acc_init+2*k, output[k]); +} + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(image_hfilter, pixel_hfilter__out_count) +{ + static const int8_t coef_count = 32; + static const unsigned output_count = 64; + static const int32_t input_stride = 4; + static const unsigned input_count = 32 + (output_count-1)*input_stride; + + int8_t coef[coef_count] = {0}; + coef[0] = 1; + + int8_t input[input_count] = {0}; + int8_t output[output_count] = {0}; + + for(int k = 0; k < input_count; k++) + input[k] = k>>2; + + const int32_t acc_init = 10; + const unsigned shift = 0; + + pixel_hfilter(output, input, coef, + acc_init, shift, input_stride, output_count); + + for(int k = 0; k < output_count; k++) + TEST_ASSERT_EQUAL_INT8(acc_init+k, output[k]); +} + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(image_hfilter, pixel_hfilter__alt_coef) +{ + static const int8_t coef_count = 32; + static const unsigned output_count = 32; + static const int32_t input_stride = 4; + static const unsigned input_count = 32 + (output_count-1)*input_stride; + + int8_t coef[coef_count] = {0}; + coef[0] = 2; + coef[1] = 4; + + int8_t input[input_count] = {0}; + int8_t output[output_count] = {0}; + int8_t expected[output_count] = {0}; + + for(int k = 0; k < input_count; k++) + input[k] = k >> 2; + + const int32_t acc_init = 16; + const unsigned shift = 0; + + for(int k = 0; k < output_count; k++){ + int expected_val = acc_init; + for(int j = 0; j < coef_count; j++) + expected_val += coef[j] * input[k*input_stride + j]; + + expected[k] = (expected_val >= INT8_MAX) ? INT8_MAX + : (expected_val <= INT8_MIN) ? INT8_MIN + : expected_val; + } + + pixel_hfilter(output, input, coef, + acc_init, shift, input_stride, output_count); + + TEST_ASSERT_EQUAL_INT8_ARRAY(expected, output, output_count); + +} + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(image_hfilter, image_hfilter_update_scale__case1) +{ + hfilter_state_t state; + + memset(&state, 0, sizeof(state)); + + const float gain = 0.0f; + + image_hfilter_update_scale(&state, gain, 0); + + const unsigned exp_shift = 9; + const int32_t exp_acc_init = -128 * (1< +#include +#include +#include +#include +#include + +#include "unity_fixture.h" + +#include "camera.h" + +TEST_GROUP_RUNNER(isp_tests) { + RUN_TEST_CASE(isp_tests, yuv_to_rgb); + RUN_TEST_CASE(isp_tests, rgb_to_yuv); +} + +TEST_GROUP(isp_tests); +TEST_SETUP(isp_tests) { fflush(stdout); } +TEST_TEAR_DOWN(isp_tests) {} + + +#define INV_DELTA 30 // error allowed in YUV RGB color conversion +#define CT_INT 127 // int conversion + +// Store the RGB color and corresponding values +typedef struct +{ + int R, G, B; + int Y, U, V; +} color_table_t; + + +#define GET_R(rgb) ((rgb >> 16) & 0xFF) +#define GET_G(rgb) ((rgb >> 8) & 0xFF) +#define GET_B(rgb) (rgb & 0xFF) + +#define GET_Y(yuv) GET_R(yuv) +#define GET_U(yuv) GET_G(yuv) +#define GET_V(yuv) GET_B(yuv) + + +void printColorTable(color_table_t* table) { + printf("Color Table:\n"); + printf("R: %d, G: %d, B: %d\n", table->R, table->G, table->B); + printf("Y: %d, U: %d, V: %d\n", table->Y, table->U, table->V); +} + +void yuv_to_rgb_ct(color_table_t* ct_ref, color_table_t* ct_res){ + *ct_res = *ct_ref; + uint32_t result = yuv_to_rgb(ct_ref->Y - CT_INT, ct_ref->U - CT_INT, ct_ref->V - CT_INT); + ct_res -> R = (uint8_t)(GET_R(result) + CT_INT); + ct_res -> G = (uint8_t)(GET_G(result) + CT_INT); + ct_res -> B = (uint8_t)(GET_B(result) + CT_INT); +} + +void rgb_to_yuv_ct(color_table_t* ct_ref, color_table_t* ct_res){ + *ct_res = *ct_ref; + uint32_t result = rgb_to_yuv(ct_ref->Y - CT_INT, ct_ref->U - CT_INT, ct_ref->V - CT_INT); + ct_res -> Y = (uint8_t)(GET_Y(result) + CT_INT); + ct_res -> U = (uint8_t)(GET_U(result) + CT_INT); + ct_res -> V = (uint8_t)(GET_V(result) + CT_INT); +} + + + +color_table_t ct_test = {16, 100, 16, 65, 100, 92}; // R G B Y U V +//TODO include more colors for testing + +TEST(isp_tests, yuv_to_rgb) +{ + // Define color table + color_table_t ct_ref = ct_test; + color_table_t ct_result; + + // Test the converison + yuv_to_rgb_ct(&ct_ref, &ct_result); + + printColorTable(&ct_test); + printColorTable(&ct_result); + + // Ensure conversion is correct + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.R, ct_result.R); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.G, ct_result.G); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.B, ct_result.B); +} + + +TEST(isp_tests, rgb_to_yuv) +{ + // Define color table + color_table_t ct_ref = ct_test; + color_table_t ct_result; + + // Test the converison + rgb_to_yuv_ct(&ct_ref, &ct_result); + + printColorTable(&ct_test); + printColorTable(&ct_result); + + // Printing the extracted bytes + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.Y, ct_result.Y); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.U, ct_result.U); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.V, ct_result.V); +} + + diff --git a/tests/unit_tests/src/test_isp.c b/tests/unit_tests/src/test_isp.c deleted file mode 100644 index befea534..00000000 --- a/tests/unit_tests/src/test_isp.c +++ /dev/null @@ -1,62 +0,0 @@ -#include -#include "test_isp.h" - -color_table_t ct_test = {16, 100, 16, 65, 100, 92}; // R G B Y U V -//TODO include more colors for testing - - -void setUp(void) -{ - -} - -void tearDown(void) -{ - -} - -void test_yuv_to_rgb() -{ - // Define color table - color_table_t ct_ref = ct_test; - color_table_t ct_result; - - // Test the converison - yuv_to_rgb_ct(&ct_ref, &ct_result); - - printColorTable(&ct_test); - printColorTable(&ct_result); - - // Ensure conversion is correct - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.R, ct_result.R); - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.G, ct_result.G); - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.B, ct_result.B); -} - -void test_rgb_to_yuv() -{ - // Define color table - color_table_t ct_ref = ct_test; - color_table_t ct_result; - - // Test the converison - rgb_to_yuv_ct(&ct_ref, &ct_result); - - printColorTable(&ct_test); - printColorTable(&ct_result); - - // Printing the extracted bytes - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.Y, ct_result.Y); - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.U, ct_result.U); - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.V, ct_result.V); -} - - -// ---------------------------------------------------------------- -int main(void) -{ - UNITY_BEGIN(); - RUN_TEST(test_rgb_to_yuv); - RUN_TEST(test_yuv_to_rgb); - return UNITY_END(); -} diff --git a/tests/unit_tests/src/test_isp.h b/tests/unit_tests/src/test_isp.h deleted file mode 100644 index afbe8c10..00000000 --- a/tests/unit_tests/src/test_isp.h +++ /dev/null @@ -1,34 +0,0 @@ -#include "unity.h" -#include "isp.h" - -#define INV_DELTA 30 // error allowed in YUV RGB color conversion -#define CT_INT 127 // int conversion - -// Store the RGB color and corresponding values -typedef struct -{ - int R, G, B; - int Y, U, V; -} color_table_t; - -void printColorTable(color_table_t* table) { - printf("Color Table:\n"); - printf("R: %d, G: %d, B: %d\n", table->R, table->G, table->B); - printf("Y: %d, U: %d, V: %d\n", table->Y, table->U, table->V); -} - -void yuv_to_rgb_ct(color_table_t* ct_ref, color_table_t* ct_res){ - *ct_res = *ct_ref; - uint32_t result = yuv_to_rgb(ct_ref->Y - CT_INT, ct_ref->U - CT_INT, ct_ref->V - CT_INT); - ct_res -> R = (uint8_t)(GET_R(result) + CT_INT); - ct_res -> G = (uint8_t)(GET_G(result) + CT_INT); - ct_res -> B = (uint8_t)(GET_B(result) + CT_INT); -} - -void rgb_to_yuv_ct(color_table_t* ct_ref, color_table_t* ct_res){ - *ct_res = *ct_ref; - uint32_t result = rgb_to_yuv(ct_ref->Y - CT_INT, ct_ref->U - CT_INT, ct_ref->V - CT_INT); - ct_res -> Y = (uint8_t)(GET_Y(result) + CT_INT); - ct_res -> U = (uint8_t)(GET_U(result) + CT_INT); - ct_res -> V = (uint8_t)(GET_V(result) + CT_INT); -} diff --git a/tests/unit_tests/src/unity_config.h b/tests/unit_tests/src/unity_config.h new file mode 100644 index 00000000..6ce67c8b --- /dev/null +++ b/tests/unit_tests/src/unity_config.h @@ -0,0 +1,10 @@ +// Copyright 2020-2022 XMOS LIMITED. +// This Software is subject to the terms of the XMOS Public Licence: Version 1. +#pragma once + +#define UNITY_SUPPORT_64 1 +#define UNITY_INCLUDE_DOUBLE 1 + +// Prevents Unity from taking control of malloc() and free() +#define UNITY_FIXTURE_NO_EXTRAS (1) + diff --git a/tests/unity.cmake b/tests/unity.cmake index 8c3b5af0..906c87ea 100644 --- a/tests/unity.cmake +++ b/tests/unity.cmake @@ -1,27 +1,22 @@ # ############################################################################## # Target name set(LIB_NAME Unity) -set(LIB_PATH ${CMAKE_CURRENT_LIST_DIR}/Unity/src) - -# Source files -file(GLOB_RECURSE SOURCES_C ${LIB_PATH}/*.c) -file(GLOB_RECURSE SOURCES_XC ${LIB_PATH}/*.xc) -file(GLOB_RECURSE SOURCES_CPP ${LIB_PATH}/*.cpp) -file(GLOB_RECURSE SOURCES_ASM ${LIB_PATH}/*.S) +set(LIB_PATH ${CMAKE_CURRENT_LIST_DIR}/Unity) add_library(${LIB_NAME} STATIC) target_include_directories( ${LIB_NAME} PUBLIC - ${LIB_PATH} + ${LIB_PATH}/src + ${LIB_PATH}/extras/fixture/src + ${LIB_PATH}/extras/memory/src ) target_sources(${LIB_NAME} - PRIVATE - ${SOURCES_C} - ${SOURCES_XC} - ${SOURCES_CPP} - ${SOURCES_ASM} + PUBLIC + ${LIB_PATH}/src/unity.c + ${LIB_PATH}/extras/fixture/src/unity_fixture.c + ${LIB_PATH}/extras/memory/src/unity_memory.c ) add_library(Unity::framework ALIAS ${LIB_NAME}) From 6c63536fa1518749d3e8a92cb42b1fc29d718fc9 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Thu, 8 Jun 2023 10:39:41 +0100 Subject: [PATCH 049/306] removing simple_timing, cleaning utils, updating raw example readme --- camera/api/utils.h | 3 - camera/src/utils.c | 26 -- examples/CMakeLists.txt | 1 - examples/simple_timing/CMakeLists.txt | 63 ---- examples/simple_timing/README.md | 13 - examples/simple_timing/src/main.xc | 31 -- examples/simple_timing/src/mipi_timing.h | 37 --- examples/simple_timing/src/mipi_timing.xc | 302 ------------------ .../take_picture_downsample/CMakeLists.txt | 2 +- examples/take_picture_raw/CMakeLists.txt | 2 +- examples/take_picture_raw/README.md | 13 - examples/take_picture_raw/README.rst | 56 ++++ examples/take_picture_raw/src/app_raw.c | 4 +- utils/CMakeLists.txt | 4 +- utils/common_utils/utils_new.h | 18 -- .../utils.c => io_utils/io_utils.c} | 9 +- utils/io_utils/io_utils.h | 17 + 17 files changed, 81 insertions(+), 520 deletions(-) delete mode 100644 examples/simple_timing/CMakeLists.txt delete mode 100644 examples/simple_timing/README.md delete mode 100644 examples/simple_timing/src/main.xc delete mode 100644 examples/simple_timing/src/mipi_timing.h delete mode 100644 examples/simple_timing/src/mipi_timing.xc delete mode 100644 examples/take_picture_raw/README.md create mode 100644 examples/take_picture_raw/README.rst delete mode 100644 utils/common_utils/utils_new.h rename utils/{common_utils/utils.c => io_utils/io_utils.c} (57%) create mode 100644 utils/io_utils/io_utils.h diff --git a/camera/api/utils.h b/camera/api/utils.h index f3eae490..a34f323a 100644 --- a/camera/api/utils.h +++ b/camera/api/utils.h @@ -30,9 +30,6 @@ void img_int8_to_uint8( uint8_t out_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS] ); -void write_image_raw(const char* filename, int8_t *image); - - #if defined(__XC__) || defined(__cplusplus) } #endif diff --git a/camera/src/utils.c b/camera/src/utils.c index 286c8b3d..7e6a8013 100644 --- a/camera/src/utils.c +++ b/camera/src/utils.c @@ -35,32 +35,6 @@ void write_image( printf("image size (%dx%d)\n", APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_HEIGHT_PIXELS); } - -/** -* Write image to a binary file containing RAW data -* -* @param filename -Name of the image -* @param image - Pointer to 1D uint8 array -*/ -void write_image_raw( - const char* filename, - int8_t *image) -{ - static FILE* img_file = NULL; - img_file = fopen(filename, "wb"); - for(uint16_t k = 0; k < MIPI_IMAGE_HEIGHT_PIXELS; k++){ - for(uint16_t j = 0; j < MIPI_LINE_WIDTH_BYTES; j++){ - uint32_t pos = k * MIPI_LINE_WIDTH_BYTES + j; - fwrite(&image[pos], sizeof(int8_t), 1, img_file); - } - } - fclose(img_file); - printf("Outfile %s\n", filename); - printf("image size (%dx%d)\n", MIPI_LINE_WIDTH_BYTES, MIPI_IMAGE_HEIGHT_PIXELS); -} - - - // This is called when want to memcpy from Xc to C void c_memcpy( void* dst, diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 4cd7fecb..a8c16350 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -2,6 +2,5 @@ set(CONFIG_XSCOPE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) # add the examples -add_subdirectory(simple_timing) add_subdirectory(take_picture_raw) add_subdirectory(take_picture_downsample) diff --git a/examples/simple_timing/CMakeLists.txt b/examples/simple_timing/CMakeLists.txt deleted file mode 100644 index f5731737..00000000 --- a/examples/simple_timing/CMakeLists.txt +++ /dev/null @@ -1,63 +0,0 @@ -#********************** -# Gather Sources -#********************** - -# <--- Set the executable -set(TARGET example_simple_timing) - -#********************** -# Flags -#********************** -set(APP_COMPILER_FLAGS - -Os - -g - -report - -fxscope - -mcmodel=large - -Wno-xcore-fptrgroup - -target=${XCORE_TARGET} - ${CONFIG_XSCOPE_PATH}/config.xscope -) - - -set(APP_COMPILE_DEFINITIONS - configENABLE_DEBUG_PRINTF=1 - PLATFORM_SUPPORTS_TILE_0=1 - PLATFORM_SUPPORTS_TILE_1=1 - PLATFORM_SUPPORTS_TILE_2=0 - PLATFORM_SUPPORTS_TILE_3=0 - PLATFORM_USES_TILE_0=1 - PLATFORM_USES_TILE_1=1 - XUD_CORE_CLOCK=600 -) - -set(APP_LINK_OPTIONS - "-report" - "-target=${XCORE_TARGET}" - "${CONFIG_XSCOPE_PATH}/config.xscope" -) - -# <--- Link libraries -set(APP_COMMON_LINK_LIBRARIES - mipi::lib_mipi - i2c::lib_i2c - sensors::lib_imx - fwk_camera::utils - fwk_camera::xscope_fileio - ) - - -#********************** -# Tile Targets -#********************** -add_executable(${TARGET} EXCLUDE_FROM_ALL) -target_sources(${TARGET} - PRIVATE - src/mipi_timing.xc - src/main.xc -) -target_include_directories(${TARGET} PUBLIC src) -target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) -target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) -target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) -target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) diff --git a/examples/simple_timing/README.md b/examples/simple_timing/README.md deleted file mode 100644 index 55573c8c..00000000 --- a/examples/simple_timing/README.md +++ /dev/null @@ -1,13 +0,0 @@ -## Example: Simple timing - -This example measures some basic timing information while taking a single frame -(see take picutre example) - -### Time measurements: -* T1 == time of a frame (without blank) -* T2 == time doing operations in a line -* T3 == time between frames -* T4 == time between lines - -## Build example -Run the following command: ```make example_timing``` diff --git a/examples/simple_timing/src/main.xc b/examples/simple_timing/src/main.xc deleted file mode 100644 index a3b4941b..00000000 --- a/examples/simple_timing/src/main.xc +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2020, XMOS Ltd, All rights reserved -#include -#include -#include - -#include "i2c.h" -#include "mipi_timing.h" - -extern "C" { -#include "xscope_io_device.h" -} - -// I2C interface ports -#define Kbps 400 -on tile[0]: port p_scl = XS1_PORT_1N; -on tile[0]: port p_sda = XS1_PORT_1O; - -int main(void) -{ - chan xscope_chan; - i2c_master_if i2c[1]; - par { - xscope_host_data(xscope_chan); - on tile[0]: i2c_master(i2c, 1, p_scl, p_sda, Kbps); - on tile[MIPI_TILE]: { - xscope_io_init(xscope_chan); - mipi_main(i2c[0]); - } - } - return 0; -} diff --git a/examples/simple_timing/src/mipi_timing.h b/examples/simple_timing/src/mipi_timing.h deleted file mode 100644 index 4e51d250..00000000 --- a/examples/simple_timing/src/mipi_timing.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include -#include "mipi.h" -#include "sensor.h" -#include -///TODO this is inside lib_mipi -#ifndef MIPI_CLKBLK -#define MIPI_CLKBLK XS1_CLKBLK_1 -#endif - -// Packets definitions -typedef struct -{ - mipi_header_t header; - uint8_t payload[MIPI_LINE_WIDTH_BYTES]; -} mipi_data_t; - -typedef struct -{ - mipi_header_t header; - uint8_t payload[MIPI_MAX_PKT_SIZE_BYTES]; -} mipi_packet_t; - -typedef struct -{ - unsigned frame_number; - unsigned line_number; -} image_rx_t; - -static mipi_packet_t packet_buffer[MIPI_PKT_BUFFER_COUNT]; - -#ifdef __XC__ - -void mipi_main(client interface i2c_master_if i2c); - -#endif diff --git a/examples/simple_timing/src/mipi_timing.xc b/examples/simple_timing/src/mipi_timing.xc deleted file mode 100644 index 50765329..00000000 --- a/examples/simple_timing/src/mipi_timing.xc +++ /dev/null @@ -1,302 +0,0 @@ -#include -#include -#include -#include // for ports -#include -#include // exit status -#include -#include -#include - -#include - - -// ********** input with measurement do you want to perform -// T1 == time of a frame (without blank) -// T2 == time doing operations in a line -// T3 == time between frames -// T4 == time between lines -#define MEASURE_T4 -#define DO_MEMCPY 0 -#define TICKS_DELAY 1 -// *********** - -// I2C -#include "i2c.h" - -// MIPI -#include "mipi_timing.h" - -// Sensor -#define MSG_SUCCESS "Stream start OK\n" -#define MSG_FAIL "Stream start Failed\n" - -// timing information -#define TIME_MSG_1 "elapsed\t%.6f\t[ms]\t%d\t[ticks]\n" -#define PRINT_TIME(y) printf("%d\n", y) -#define MS_MULTIPLIER 0.00001 - -// Utils -#include "utils.h" - -// Globals -#define FINAL_IMAGE_FILENAME "out.raw" -uint8_t FINAL_IMAGE[MIPI_IMAGE_HEIGHT_PIXELS][MIPI_LINE_WIDTH_BYTES]; -char end_transmission = 0; -char found = 0; -mipi_data_type_t p_data_type = 0; -int t1, t2, t3, t4, t5, t6; - -/** -* Declaration of the MIPI interface ports: -* Clock, receiver active, receiver data valid, and receiver data -*/ -on tile[MIPI_TILE] : in port p_mipi_clk = XS1_PORT_1O; -on tile[MIPI_TILE] : in port p_mipi_rxa = XS1_PORT_1E; // activate -on tile[MIPI_TILE] : in port p_mipi_rxv = XS1_PORT_1I; // valid -on tile[MIPI_TILE] : buffered in port:32 p_mipi_rxd = XS1_PORT_8A; // data -on tile[MIPI_TILE] : clock clk_mipi = MIPI_CLKBLK; - -/** - * The packet buffer is where the packet decoupler will tell the MIPI receiver - * thread to store received packets. - */ -#define DEMUX_DATATYPE 0 // RESERVED -#define DEMUX_MODE 0x00 // no demux -#define DEMUX_EN 0 // DISABLE DEMUX -#define MIPI_CLK_DIV 1 // CLK DIVIDER -#define MIPI_CFG_CLK_DIV 3 // CFG DIVIDER -#define REV(n) ((n << 24) | (((n>>16)<<24)>>16) | (((n<<16)>>24)<<16) | (n>>24)) - - -// Saves the image to a file. This is a bit tricky because we don't want to use an endless loop -void save_image_to_file(chanend flag) -{ - select - { - case flag :> int i: - { - // write to a file - write_image(FINAL_IMAGE_FILENAME, &FINAL_IMAGE[0][0], MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES); - delay_microseconds(200); // for stability - exit(1); // end the program here - break; - } - } -} - -//TODO this should be in utils -int measure_time(){ - int y = 0; - asm volatile("gettime %0": "=r"(y)); - return y; -} - -unsafe { -static -void handle_packet( - image_rx_t* img_rx, - const mipi_packet_t* unsafe pkt, - chanend flag) - { - - // definitions - const mipi_header_t header = pkt->header; - const mipi_data_type_t data_type = MIPI_GET_DATA_TYPE(header); - const unsigned is_long = MIPI_IS_LONG_PACKET(header); - const unsigned word_count = MIPI_GET_WORD_COUNT(header); - static uint8_t wait_for_clean_frame = 1; // static because it will change in the future - //[debug] printf("packet header = 0x%08x, wc=%d \n", REV(header), word_count); - - - // We return until the start of frame is reached - if (wait_for_clean_frame == 1){ - if (data_type != MIPI_DT_FRAME_START){ - return; - } - else{ - wait_for_clean_frame = 0; - } - } - // Use case by data type - switch (data_type) - { - case MIPI_DT_FRAME_START: { // Start of frame. Just reset line number. - #ifdef MEASURE_T3 - PRINT_TIME((measure_time() - t3)); - t3 = measure_time(); - #endif - - #ifdef MEASURE_T1 - t1 = measure_time(); - #endif - - img_rx->frame_number++; - img_rx->line_number = 0; - break; - } - - case EXPECTED_FORMAT:{ // save it in SRAM and increment line - - #ifdef MEASURE_T4 - PRINT_TIME((measure_time() - t4)); - t4 = measure_time(); - #endif - - // if line number is grater than expected, just reset the line number - if (img_rx->line_number >= MIPI_IMAGE_HEIGHT_PIXELS) - { - break; // let pass the rest until next frame - } - - #if DO_MEMCPY - // then copy - not_silly_memcpy( - &FINAL_IMAGE[img_rx->line_number][0], - &pkt->payload[0], - MIPI_LINE_WIDTH_BYTES); // here is data width - #endif - - // burn some cycles - delay_ticks(TICKS_DELAY); - - // printf("0x%04x,",pkt->payload[0]); - - // go for next line and exit - img_rx->line_number++; - - break; - } - - case MIPI_DT_EOT:{ - - break; - } - - case MIPI_DT_FRAME_END:{ // we signal that the frame is finish so we can write it to a file - // printf("--------->> End\n"); - #ifdef MEASURE_T1 - PRINT_TIME((measure_time() - t1)); - #endif - break; - } - - default: // error with frame type or protected types - { - break; - } - } - } - - -#pragma unsafe arrays -static -void mipi_packet_handler( - streaming chanend c_pkt, - streaming chanend c_ctrl, - chanend flag - ) -{ - image_rx_t img_rx = {0,0}; // stores the coordinates X, Y of the image - unsigned pkt_idx = 0; // packet index - - // Give the MIPI packet receiver a buffer - outuint((chanend) c_pkt, (unsigned) &packet_buffer[pkt_idx]); - pkt_idx = (pkt_idx + 1) & (MIPI_PKT_BUFFER_COUNT-1); - - while(1) { - - #ifdef MEASURE_T6 - t6 = measure_time(); - #endif - - - // Wait for the receiver thread to tell us a new packet was completed. - mipi_packet_t * unsafe pkt = (mipi_packet_t*unsafe) inuint((chanend) c_pkt); - - - // Give it a new buffer before processing the received one - outuint((chanend) c_pkt, (unsigned) &packet_buffer[pkt_idx]); - pkt_idx = (pkt_idx + 1) & (MIPI_PKT_BUFFER_COUNT-1); - - #ifdef MEASURE_T6 - PRINT_TIME((measure_time() - t6)); - #endif - - // Process the packet. We need to be finished with this and looped - // back up to grab the next MIPI packet BEFORE the receiver thread - // tries to give us the next packet. - #ifdef MEASURE_T2 - t4 = measure_time(); - #endif - - handle_packet(&img_rx, pkt, flag); - - - #ifdef MEASURE_T2 - PRINT_TIME((measure_time() - t4)); - #endif - } -} -} - - -void mipi_main(client interface i2c_master_if i2c) -{ - printf("< Start of APP capture application >\n"); - streaming chan c_pkt; - streaming chan c_ctrl; - chan flag; - - - // See AN for MIPI shim - // 0x7E42 >> 0111 1110 0100 0010 - // in the explorer BOARD DPDN is swap - write_node_config_reg(tile[MIPI_TILE], - XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, - 0x7E42); //TODO decompose into different values - - // send packet to MIPI shim - MipiPacketRx_init(tile[MIPI_TILE], - p_mipi_rxd, - p_mipi_rxv, - p_mipi_rxa, - p_mipi_clk, - clk_mipi, - DEMUX_EN, - DEMUX_DATATYPE, - DEMUX_MODE, - MIPI_CLK_DIV, - MIPI_CFG_CLK_DIV); - - // Start camera and its configurations - int r = 0; - r |= camera_init(i2c); - delay_milliseconds(100); //TODO include this inside the function - r |= camera_configure(i2c); - delay_milliseconds(500); - - // Start streaming mode - r |= camera_start(i2c); - delay_milliseconds(2000); - - if (r != 0){ - printf(MSG_FAIL); - } - else{ - printf(MSG_SUCCESS); - } - - // ask the user to press the key "c" to capture the frame - // user_input(); //TODO not working with a par job - - - // start the different jobs (packet controller, handler, and post_process) - par - { - MipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); - mipi_packet_handler(c_pkt, c_ctrl, flag); - save_image_to_file(flag); - } -} - diff --git a/examples/take_picture_downsample/CMakeLists.txt b/examples/take_picture_downsample/CMakeLists.txt index a9a78d24..d2eeca56 100644 --- a/examples/take_picture_downsample/CMakeLists.txt +++ b/examples/take_picture_downsample/CMakeLists.txt @@ -50,7 +50,7 @@ set(APP_COMMON_LINK_LIBRARIES #********************** # Tile Targets #********************** -add_executable(${TARGET} EXCLUDE_FROM_ALL) +add_executable(${TARGET}) target_sources(${TARGET} PRIVATE src/app.c diff --git a/examples/take_picture_raw/CMakeLists.txt b/examples/take_picture_raw/CMakeLists.txt index 6cf7d109..acf48a4d 100644 --- a/examples/take_picture_raw/CMakeLists.txt +++ b/examples/take_picture_raw/CMakeLists.txt @@ -53,7 +53,7 @@ set(APP_COMMON_LINK_LIBRARIES #********************** # Tile Targets #********************** -add_executable(${TARGET} EXCLUDE_FROM_ALL) +add_executable(${TARGET}) target_sources(${TARGET} PRIVATE src/app_raw.c diff --git a/examples/take_picture_raw/README.md b/examples/take_picture_raw/README.md deleted file mode 100644 index cf6ae175..00000000 --- a/examples/take_picture_raw/README.md +++ /dev/null @@ -1,13 +0,0 @@ -## Example: Take picture - -This example set the basic settings for the sony sensor and grab a single frame. -By default the format is the following: -* 640x480 RAW8 - -### File Description -* Sensor.h : configures and set the basic settings for the sensor -* Main.xc : starts 2 threads, i2C control and mipi capture -* Process_frame.c : write the image to a file - -## Build example -Run the following command: ```make example_take_picture``` \ No newline at end of file diff --git a/examples/take_picture_raw/README.rst b/examples/take_picture_raw/README.rst new file mode 100644 index 00000000..028031ca --- /dev/null +++ b/examples/take_picture_raw/README.rst @@ -0,0 +1,56 @@ +===================== +Example: Take picture +===================== + +This example set the basic settings for the sony sensor and grab a single frame. +By default the format is the following: +- 640x480 RAW8 + +************* +Build example +************* +Run the following commands from the top level: + +.. tab:: Linux and Mac + + .. code-block:: console + + cmake -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -B build + make -C build example_take_picture_raw + +.. tab:: Windows + + .. code-block:: console + + cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -B build + ninja -C example_take_picture_raw + +*************** +Running example +*************** + +From the top level + +.. tab:: Linux and Mac + + .. code-block:: console + + pip install -e utils/xscope_fileio + python python/run_xscope_bin.py build/examples/take_picture_raw/example_take_picture_raw.xe + +.. tab:: Windows + + .. code-block:: console + + pip install -e utils/xscope_fileio + cd utils/xscope_fileio/host + cmake -G "Ninja" . && ninja + cd ../../../ + python python/run_xscope_bin.py build/examples/take_picture_raw/example_take_picture_raw.xe + + +****** +Output +****** + +The output file ``capture.bin`` will be generated at the top level the repository. It can be further processed using ``python/decode_raw8.py`` script. diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index 37f5d333..1aadd36c 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -8,7 +8,7 @@ //#include "utils.h" #include "user_api.h" #include "app_raw.h" -#include "utils_new.h" +#include "io_utils.h" void user_app_raw(streaming_chanend_t c_cam_api){ @@ -26,6 +26,6 @@ void user_app_raw(streaming_chanend_t c_cam_api){ // Save the image to a file size_t img_size = MIPI_IMAGE_HEIGHT_PIXELS * MIPI_LINE_WIDTH_BYTES * sizeof(uint8_t); - write_image_new("capture.bin", (uint8_t * ) &image_buffer[0], img_size, MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES); + write_image_file("capture.bin", (uint8_t * ) &image_buffer[0], img_size, MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES); exit(0); } diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 2268adee..b907873d 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -4,8 +4,8 @@ include(xscope_fileio.cmake) set(LIB_NAME fwk_camera_utils) add_library(${LIB_NAME} INTERFACE) -target_sources(${LIB_NAME} INTERFACE common_utils/utils.c) +target_sources(${LIB_NAME} INTERFACE io_utils/io_utils.c) -target_include_directories(${LIB_NAME} INTERFACE common_utils) +target_include_directories(${LIB_NAME} INTERFACE io_utils) add_library(fwk_camera::utils ALIAS ${LIB_NAME}) diff --git a/utils/common_utils/utils_new.h b/utils/common_utils/utils_new.h deleted file mode 100644 index 13b17569..00000000 --- a/utils/common_utils/utils_new.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include -#include -#include - -#ifdef __XC__ -extern "C" { -#endif - -#include "xscope_io_device.h" - -void write_image_new(char * filename, uint8_t * image, const size_t size, const size_t height, const size_t width); -void c_memcpy(void * dst, void * src, size_t size); - -#ifdef __XC__ -} -#endif diff --git a/utils/common_utils/utils.c b/utils/io_utils/io_utils.c similarity index 57% rename from utils/common_utils/utils.c rename to utils/io_utils/io_utils.c index d224ea8b..31940839 100644 --- a/utils/common_utils/utils.c +++ b/utils/io_utils/io_utils.c @@ -1,8 +1,8 @@ -#include "utils_new.h" +#include "io_utils.h" #include -void write_image_new(char * filename, uint8_t * image, const size_t size, const size_t height, const size_t width) +void write_image_file(char * filename, uint8_t * image, const size_t size, const size_t height, const size_t width) { xscope_file_t fp = xscope_open_file(filename, "wb"); @@ -13,8 +13,3 @@ void write_image_new(char * filename, uint8_t * image, const size_t size, const printf("Image written into file: %s\n", filename); printf("Image dimentions: %d x %d\n", width, height); } - -void c_memcpy(void * dst, void * src, size_t size) -{ - memcpy(dst, src, size); -} diff --git a/utils/io_utils/io_utils.h b/utils/io_utils/io_utils.h new file mode 100644 index 00000000..cf6a0c49 --- /dev/null +++ b/utils/io_utils/io_utils.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +#ifdef __XC__ +extern "C" { +#endif + +#include "xscope_io_device.h" + +void write_image_file(char * filename, uint8_t * image, const size_t size, const size_t height, const size_t width); + +#ifdef __XC__ +} +#endif From a3a24d1e239b65089d6224abf1c599e38f66f2c0 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Thu, 8 Jun 2023 11:03:51 +0100 Subject: [PATCH 050/306] adding app_test to test folder --- tests/CMakeLists.txt | 4 + tests/config.xscope | 22 + tests/hardware_tests/CMakeLists.txt | 2 + .../hardware_tests/test_timing/CMakeLists.txt | 69 +++ .../test_timing/src/MipiGatherTiming.S | 201 +++++++ tests/hardware_tests/test_timing/src/app.h | 18 + tests/hardware_tests/test_timing/src/app.xc | 556 ++++++++++++++++++ tests/hardware_tests/test_timing/src/main.xc | 29 + 8 files changed, 901 insertions(+) create mode 100644 tests/config.xscope create mode 100644 tests/hardware_tests/CMakeLists.txt create mode 100644 tests/hardware_tests/test_timing/CMakeLists.txt create mode 100644 tests/hardware_tests/test_timing/src/MipiGatherTiming.S create mode 100644 tests/hardware_tests/test_timing/src/app.h create mode 100644 tests/hardware_tests/test_timing/src/app.xc create mode 100644 tests/hardware_tests/test_timing/src/main.xc diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8e6aee61..06a230e4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,2 +1,6 @@ +# set xscope config file path for tests +set(CONFIG_XSCOPE_PATH_TEST ${CMAKE_CURRENT_SOURCE_DIR}) + add_subdirectory(unit_tests) +add_subdirectory(hardware_tests) include(unity.cmake) # custom cmake file diff --git a/tests/config.xscope b/tests/config.xscope new file mode 100644 index 00000000..69236026 --- /dev/null +++ b/tests/config.xscope @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/hardware_tests/CMakeLists.txt b/tests/hardware_tests/CMakeLists.txt new file mode 100644 index 00000000..61b3c277 --- /dev/null +++ b/tests/hardware_tests/CMakeLists.txt @@ -0,0 +1,2 @@ +# Add tests directories +add_subdirectory(test_timing) diff --git a/tests/hardware_tests/test_timing/CMakeLists.txt b/tests/hardware_tests/test_timing/CMakeLists.txt new file mode 100644 index 00000000..b2ddfad4 --- /dev/null +++ b/tests/hardware_tests/test_timing/CMakeLists.txt @@ -0,0 +1,69 @@ +#********************** +# Gather Sources +#********************** + +# <--- Set the executable +set(TARGET test_timing) + +set(APP_INCLUDES + ${CMAKE_CURRENT_LIST_DIR}/src +) + +file(GLOB_RECURSE APP_SOURCES + ${CMAKE_CURRENT_LIST_DIR}/src/*.c + ${CMAKE_CURRENT_LIST_DIR}/src/*.xc + ${CMAKE_CURRENT_LIST_DIR}/src/*.S +) + +#********************** +# Flags +#********************** +set(APP_COMPILER_FLAGS + -Os + -g + -report + -fxscope + -mcmodel=large + -Wno-xcore-fptrgroup + -target=${XCORE_TARGET} + ${CONFIG_XSCOPE_PATH_TEST}/config.xscope +) + + +set(APP_COMPILE_DEFINITIONS + configENABLE_DEBUG_PRINTF=1 + PLATFORM_SUPPORTS_TILE_0=1 + PLATFORM_SUPPORTS_TILE_1=1 + PLATFORM_SUPPORTS_TILE_2=0 + PLATFORM_SUPPORTS_TILE_3=0 + PLATFORM_USES_TILE_0=1 + PLATFORM_USES_TILE_1=1 + XUD_CORE_CLOCK=600 +) + +set(APP_LINK_OPTIONS + "-report" + "-target=${XCORE_TARGET}" + "${CONFIG_XSCOPE_PATH}/config.xscope" +) + + +# <--- Link libraries +set(APP_COMMON_LINK_LIBRARIES + mipi::lib_mipi + i2c::lib_i2c + sensors::lib_imx + camera::lib_camera + ) + + +#********************** +# Tile Targets +#********************** +add_executable(${TARGET} EXCLUDE_FROM_ALL) +target_sources(${TARGET} PUBLIC ${APP_SOURCES}) +target_include_directories(${TARGET} PUBLIC ${APP_INCLUDES}) +target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) +target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) +target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) +target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) diff --git a/tests/hardware_tests/test_timing/src/MipiGatherTiming.S b/tests/hardware_tests/test_timing/src/MipiGatherTiming.S new file mode 100644 index 00000000..b411bb9c --- /dev/null +++ b/tests/hardware_tests/test_timing/src/MipiGatherTiming.S @@ -0,0 +1,201 @@ + +#include +#include + +/** + * + * This function is a way to gather timing information about a device so that + * its behavior can be reasoned about. + * + * The output is a table uint32_t[N][3], where each row corresponds to one + * received packet. The first column is the packet header (containing data type + * and packet length). The second column is when the header was received. The + * third column is when the end of the packet is received. + * + * This function returns when the table has been filled. + * + * This function throws away all packet data other than the headers. + * +*/ + +// Wait for EOF before populating the packet log. Useful to ensure a clean log. +#ifndef WAIT_FOR_EOF +#define WAIT_FOR_EOF (1) +#endif + + +.issue_mode dual + +#define FUNCTION_NAME MipiGatherTiming +#define NSTACKWORDS 8 + +.globl FUNCTION_NAME.nstackwords +.globl FUNCTION_NAME.maxthreads +.globl FUNCTION_NAME.maxtimers +.globl FUNCTION_NAME.maxchanends + +.linkset FUNCTION_NAME.nstackwords, NSTACKWORDS +.linkset FUNCTION_NAME.maxchanends, 0 +.linkset FUNCTION_NAME.maxtimers, 0 +.linkset FUNCTION_NAME.maxthreads, 0 + +.globl FUNCTION_NAME +.type FUNCTION_NAME, @function +.text +.cc_top FUNCTION_NAME.func, FUNCTION_NAME + +#define P_RXD r0 +#define P_RXA r1 +#define TABLE r2 +#define LEN r3 + +#define HEADER r4 +#define S0 r5 +#define S1 r6 +#define _12 r7 + + +/**************************************************** + **************************************************** + + typedef struct { + uint32_t header; + uint32_t start_time; + uint32_t end_time; + } packet_timing_t; + + + void MipiGatherTiming( + const unsigned RxD, + const unsigned RxA, + packet_timing_t table[], + const unsigned N); + + **************************************************** + **************************************************** +*/ + +.align 4 +.skip 0 +FUNCTION_NAME: + dualentsp NSTACKWORDS + std r4, r5, sp[1] + std r6, r7, sp[2] + std r8, r9, sp[3] + + +// Enable MIPI shim hardware +{ ldc r4, 0x01 ; ldc r9, 8 } +{ shl r4, r4, r9 ; sub LEN, LEN, 1 } + ldc r9, XS1_PS_XCORE_CTRL0 + get r11, ps[r9] +{ or r4, r4, r11 ; ldc _12, 12 } + set ps[r9], r4 + + +// Enable RxA event + setc res[P_RXA], XS1_SETC_COND_EQ // <-- event on logic value of RxA + setc res[P_RXA], XS1_SETC_IE_MODE_EVENT // <-- do event, not interrupt + ldc r11, 0 + setd res[P_RXA], r11 // <-- condition is low logic value + ldap r11, .L_MIPI_RXA_LOW + setv res[P_RXA], r11 + +// Enable events for the thread + setsr 1 // Sets the EEBLE bit (I couldn't find a #define!) + +#if (WAIT_FOR_EOF) + + ldap r11, .L_MULP + setv res[P_RXA], r11 + +// Wait to receive an end-of-frame packet before collecting data +.L_BLORT: +{ in HEADER, res[P_RXD] ; ldc S1, 0x00000030 } +{ zext HEADER, 6 ; and S1, S1, HEADER } +{ eet S1, res[P_RXA] ; bt S1, .L_SPOOT } +{ eq S1, HEADER, 1 ; } +{ ; bf S1, .L_BLORT } +// It is an EOF packet + ldap r11, .L_MIPI_RXA_LOW +{ setv res[P_RXA], r11 ; bu .L_GOT_EOF } + + +.L_SPOOT: // long packet. Just burn the data + // an RxA low event will break us out + { in S0, res[P_RXD] ; } + { ; bu .L_SPOOT } + + +.align 32 +.L_MULP: + +{ in r11, res[P_RXA] ; } +{ endin r11, res[P_RXD] ; } +{ in S1, res[P_RXD] ; } +{ edu res[P_RXA] ; } +{ setsr 1 ; bu .L_BLORT } + +.L_GOT_EOF: + +#else + +// Check whether the RxA signal is currently high. +// If so, we need to wait until it is low or we will start reading mid-packet +.L_WAIT_LOOP: +{ peek S0, res[P_RXA] ; } +{ ; bt S0, .L_WAIT_LOOP } + +#endif + +.L_WAIT_FOR_NEXT_PACKET: + +// Receive packet header from data port +{ in HEADER, res[P_RXD] ; ldc S1, 0x00000030 } +{ gettime r11 ; stw HEADER, TABLE[0] } +{ and S1, S1, HEADER ; stw r11, TABLE[1] } + +// If short packet, duplicate timestamp into column 3. +// Otherwise, wait for end of packet. +{ eet S1, res[P_RXA] ; bt S1, .L_RX_LONG } + +.L_RX_SHORT: + +{ add TABLE, TABLE, _12 ; stw r11, TABLE[2] } +{ sub LEN, LEN, 1 ; bf LEN, .L_EXIT_FUNC } +{ ; bu .L_WAIT_FOR_NEXT_PACKET } + + +.L_RX_LONG: +// Burn through the rest of the packet. We don't care about the payload. +.L_RX_LONG_LOOP: + { in S0, res[P_RXD] ; } + { ; bu .L_RX_LONG_LOOP } + + +.align 32 +.L_MIPI_RXA_LOW: + +{ gettime r11 ; } +{ in r11, res[P_RXA] ; stw r11, TABLE[2] } +{ endin r11, res[P_RXD] ; add TABLE, TABLE, _12 } +{ in S1, res[P_RXD] ; } +{ edu res[P_RXA] ; } +{ sub LEN, LEN, 1 ; bf LEN, .L_EXIT_FUNC } +{ setsr 1 ; bu .L_WAIT_FOR_NEXT_PACKET } + + +.L_EXIT_FUNC: + + // This doesn't actually undo the event enabling and whatnot, but that's fine. + + ldd r8, r9, sp[3] + ldd r6, r7, sp[2] + ldd r4, r5, sp[1] + retsp NSTACKWORDS + +.size FUNCTION_NAME, .-FUNCTION_NAME +.cc_bottom FUNCTION_NAME.func + + + diff --git a/tests/hardware_tests/test_timing/src/app.h b/tests/hardware_tests/test_timing/src/app.h new file mode 100644 index 00000000..80a8e8e4 --- /dev/null +++ b/tests/hardware_tests/test_timing/src/app.h @@ -0,0 +1,18 @@ + +#pragma once + +#include + +#include "mipi.h" +#include "camera.h" + +#ifndef MIPI_CLKBLK +# define MIPI_CLKBLK XS1_CLKBLK_1 +#endif + + +#ifdef __XC__ + +void mipi_main(client interface i2c_master_if i2c); + +#endif diff --git a/tests/hardware_tests/test_timing/src/app.xc b/tests/hardware_tests/test_timing/src/app.xc new file mode 100644 index 00000000..d67cbb29 --- /dev/null +++ b/tests/hardware_tests/test_timing/src/app.xc @@ -0,0 +1,556 @@ +// Copyright (c) 2020, XMOS Ltd, All rights reserved + +#include +#include +#include +#include +#include +#include +#include +#include +#include "i2c.h" +#include "mipi.h" +#include "app.h" +#include "sensor.h" + +#include "mipi.h" +#include "xccompat.h" + +#include +#include +#include + +#include "i2c.h" +#include "app.h" + +// Sensor +#define MSG_SUCCESS "Stream start OK\n" +#define MSG_FAIL "Stream start Failed\n" + + +//////////////////////////////////////////////////////////////// +// if true, print warnings when unexpected packet sequences are observed +#ifndef SEQUENCE_WARNINGS +# define SEQUENCE_WARNINGS (0) +#endif + +// if true, don't assume we started listening for packets outside of a frame, +// so suppress any warnings until we see an SoF. +#ifndef IGNORE_SEQ_WARN_BEFORE_SOF +# define IGNORE_SEQ_WARN_BEFORE_SOF (0) +#endif + +/* + astew: I'm just trying out the stuff below to decide whether I think it's + awful +*/ +#define WARNING(COND) for(int fgsfds = 0; (COND) && (fgsfds < 1); fgsfds++) +#define ONLY_IF(COND) if(!(COND)) break +#define NOT_IF(COND) if((COND)) break + + +#ifndef REBASE_TIMESTAMPS +# define REBASE_TIMESTAMPS (1) +#endif + +#ifndef PRINT_LOG_SUMMARY +# define PRINT_LOG_SUMMARY (1) +#endif + +#ifndef PRINT_TIMING_STATS +# define PRINT_TIMING_STATS (1) +#endif + +#ifndef WRITE_LOG_TO_FILE +# define WRITE_LOG_TO_FILE (1) +#endif + + +#ifndef PACKET_LOG_FILE +# define PACKET_LOG_FILE "mipi_packet_log.csv" +#endif + + +#define TABLE_ROWS (12020) +//////////////////////////////////////////////////////////////// + +// Start port declarations +/* Declaration of the MIPI interface ports: + * Clock, receiver active, receiver data valid, and receiver data + */ +on tile[MIPI_TILE]: in port p_mipi_clk = XS1_PORT_1O; +on tile[MIPI_TILE]: in port p_mipi_rxa = XS1_PORT_1E; +on tile[MIPI_TILE]: in port p_mipi_rxv = XS1_PORT_1I; +on tile[MIPI_TILE]:buffered in port:32 p_mipi_rxd = XS1_PORT_8A; + +on tile[MIPI_TILE]:clock clk_mipi = MIPI_CLKBLK; + + +// Inclusive between +static inline +unsigned between(unsigned low, unsigned x, unsigned high){ + return (x >= low) && (x <= high); +} + + + +typedef struct { + uint32_t header; + uint32_t start_time; + uint32_t end_time; +} packet_timing_t; + +// Function to grab timing info +void MipiGatherTiming( + buffered in port:32 p_mipi_rxd, + in port p_mipi_rxa, + packet_timing_t table[], + const unsigned N); + + + +// Basically just subtract the first start time from every timestamp so that +// the first packet says it starts at 0. +static +uint32_t rebaseTimestamps( + packet_timing_t packet[], + unsigned N) +{ + const uint32_t time_offset = packet[0].start_time; + for(int k = 0; k < N; k++){ + packet[k].start_time -= time_offset; + packet[k].end_time -= time_offset; + } + + return time_offset; +} + + +typedef struct { + uint32_t min; + uint32_t max; + uint64_t total; + uint32_t count; +} timing_stats_t; + + + +static +void updateTimingStats( + timing_stats_t* stats, + uint32_t timespan) +{ + if(stats->count == 0){ + stats->min = stats->max = timespan; + } else { + stats->min = (timespan < stats->min)? timespan : stats->min; + stats->max = (timespan > stats->max)? timespan : stats->max; + } + + stats->count++; + stats->total += timespan; +} + + + +typedef struct { + struct { + uint32_t start_of_frame; + uint32_t end_of_frame; + uint32_t yuv_data; + uint32_t raw_data; + uint32_t generic_long; + uint32_t other_short; + uint32_t other_long; + } packet_count; + + struct { + // Time between Start of Frame packet and End of Frame packet + timing_stats_t SOF_to_EOF; + // Time between the beginning of Start of Frame packet and the next + // Start of Frame packet + timing_stats_t SOF_to_SOF; + // Time between the beginning of one packet of line data and the + // beginning of the next packet of line data + timing_stats_t SOL_to_SOL; + // Time between line data packet header and end of line data packet + timing_stats_t SOL_to_EOL; + } timing; +} mipi_timing_info_t; + + +static +mipi_timing_info_t extractTimingInfo( + packet_timing_t packet[], + unsigned N) +{ + // Used for tracking inter-packet timing. + // If any are 0, that means they haven't been observed yet. + struct { + unsigned SoF; + unsigned EoF; + unsigned SoL; + unsigned EoL; + } last = {0}; + + mipi_timing_info_t result; + memset(&result, 0, sizeof(mipi_timing_info_t)); + + // Warn if we're seeing an odd sequence + unsigned inside_frame = 0; + + for(int k = 0; k < N; k++){ + + packet_timing_t* pkt = &packet[k]; + + unsigned is_long = MIPI_IS_LONG_PACKET(pkt->header)? 1:0; + unsigned data_type = MIPI_GET_DATA_TYPE(pkt->header); + + unsigned duration = pkt->end_time - pkt->start_time; + + if(!is_long){ + // Short packet + assert(duration == 0); + + if(data_type == MIPI_DT_FRAME_START){ + + ///// Start of Frame packet + + // for(int fgsfds = 0; (inside_frame) && (fgsfds < 1); fgsfds++) { + WARNING(inside_frame) { + ONLY_IF (SEQUENCE_WARNINGS); + + printf("Warning: Observed SoF while inside frame." + " (Log Index: %u)\n", k); + } + + if(last.SoF){ + unsigned delta = pkt->start_time - last.SoF; + updateTimingStats(&result.timing.SOF_to_SOF, delta); + } + + last.SoF = pkt->start_time; + result.packet_count.start_of_frame++; + inside_frame = 1; + + } else if(data_type == MIPI_DT_FRAME_END) { + + ///// End of Frame packet + + WARNING(!inside_frame) { + ONLY_IF (SEQUENCE_WARNINGS); + NOT_IF (last.SoF == 0 && IGNORE_SEQ_WARN_BEFORE_SOF); + + printf("Warning: Observed EoF while outside frame." + " (Log Index: %u)\n", k); + } + + if(last.SoF){ + unsigned delta = pkt->start_time - last.SoF; + updateTimingStats(&result.timing.SOF_to_EOF, delta); + } + + last.EoF = pkt->start_time; + result.packet_count.end_of_frame++; + inside_frame = last.SoL = last.EoL = 0; + + } else { + + ///// Some other short packet + result.packet_count.other_short++; + } + + + } else { + // Long packet + assert(duration); + + WARNING(!inside_frame) { + ONLY_IF (SEQUENCE_WARNINGS); + NOT_IF (last.SoF == 0 && IGNORE_SEQ_WARN_BEFORE_SOF); + + printf("Warning: Observed long packet while outside frame." + " (Log Index: %u)\n", k); + } + + + if(between(MIPI_DT_YUV420_8BIT, data_type, MIPI_DT_YUV422_10BIT) ){ + // yuv packet + result.packet_count.yuv_data++; + } else if(between(MIPI_DT_NULL, data_type, MIPI_DT_RESERVED_0x17)){ + // Generic long packet + result.packet_count.generic_long++; + } else if(between(MIPI_DT_RAW24, data_type, MIPI_DT_RAW20)){ + // raw packet + result.packet_count.raw_data++; + } else { + // other long packet + result.packet_count.other_long++; + } + + if(last.SoL){ + unsigned delta = pkt->start_time - last.SoL; + updateTimingStats(&result.timing.SOL_to_SOL, delta); + } + + updateTimingStats(&result.timing.SOL_to_EOL, duration); + + last.SoL = pkt->start_time; + last.EoL = pkt->end_time; + } + } + + return result; +} + + +static +void printTimingInfo( + mipi_timing_info_t* info) +{ + unsigned total_pkt = info->packet_count.start_of_frame + + info->packet_count.end_of_frame + + info->packet_count.yuv_data + + info->packet_count.generic_long + + info->packet_count.raw_data + + info->packet_count.other_short + + info->packet_count.other_long; + + printf("#### Packet Counts ####\n"); + printf(" Total: % 5u\n", total_pkt); + printf("\n"); + printf(" ## Short ##\n"); + printf(" Frame Start: % 5u\n", info->packet_count.start_of_frame); + printf(" Frame End: % 5u\n", info->packet_count.end_of_frame ); + printf(" Other Short: % 5u\n", info->packet_count.other_short ); + printf("\n"); + printf(" ## Long ##\n"); + printf(" YUV Data: % 5u\n", info->packet_count.yuv_data ); + printf(" Generic Long:% 5u\n", info->packet_count.generic_long ); + printf(" RAW Data: % 5u\n", info->packet_count.raw_data ); + printf(" Other Long: % 5u\n", info->packet_count.other_long ); + + printf("\n\n"); + printf("#### Timing Info ####\n\n"); + + printf(" | Min (us) | Ave (us) | Max (us) \n"); + printf("-------------|--------------|--------------|--------------\n"); + + #define FMT "| % 12.2f | % 12.2f | % 12.2f" + + printf(" SoF to SoF " FMT "\n", + info->timing.SOF_to_SOF.min * 0.01, + info->timing.SOF_to_SOF.total / (100.0 * info->timing.SOF_to_SOF.count), + info->timing.SOF_to_SOF.max * 0.01); + + printf(" SoF to EoF " FMT "\n", + info->timing.SOF_to_EOF.min * 0.01, + info->timing.SOF_to_EOF.total / (100.0 * info->timing.SOF_to_EOF.count), + info->timing.SOF_to_EOF.max * 0.01); + + printf(" SoL to SoL " FMT "\n", + info->timing.SOL_to_SOL.min * 0.01, + info->timing.SOL_to_SOL.total / (100.0 * info->timing.SOL_to_SOL.count), + info->timing.SOL_to_SOL.max * 0.01); + + printf(" SoL to EoL " FMT "\n", + info->timing.SOL_to_EOL.min * 0.01, + info->timing.SOL_to_EOL.total / (100.0 * info->timing.SOL_to_EOL.count), + info->timing.SOL_to_EOL.max * 0.01); + + #undef FMT + + printf("\n Note: Thread MIPS is also the number of instructions per microsecond.\n"); + printf( " Scaling above times by thread MIPS gives instruction budget.\n"); + +} + + +static +void writePacketLog( + const char* filename, + packet_timing_t packet[], + unsigned N) +{ + + FILE * movable log_file = fopen(filename, "w"); + + if(!log_file){ + printf("\n\nWARNING: Couldn't open '%s' to write packet log.\n\n", filename); + return; + } + + for(int k = 0; k < N; k++){ + fprintf(log_file, "0x%08X,0x%08X,0x%08X\n", + packet[k].header, packet[k].start_time, packet[k].end_time); + } + + fclose(move(log_file)); +} + + +static inline +unsigned can_aggregate(mipi_header_t a, mipi_header_t b) +{ + return (MIPI_GET_DATA_TYPE(a) == MIPI_GET_DATA_TYPE(b)) + && (MIPI_GET_WORD_COUNT(a) == MIPI_GET_WORD_COUNT(b)); +} + + + +// Instead of printing full log, summarize it.. +// - roll up a sequence of consecutive "line" packets into a single line of text +static +void printPacketLogSummary( + packet_timing_t packet[], + unsigned N) +{ + + printf("#### Packet Log Summary ####\n\n"); + + printf(" Packet | Data | Word | Gap | Start | Duration | End | Packet | \n"); + printf(" Index | Type | Count | Time (us) | Time (us) | (us) | Time (us) | Count | Misc\n"); + printf("--------|------|-------|------------|------------|------------|------------|--------|-----\n"); + +#define FMT " %5u | 0x%02X | % 5u | % 10.2f | % 10.2f | % 10.2f | % 10.2f | % 4u | %s\n" + + double prev_end_time_us = 0; + + char str_extra[100] = {0}; + + int k = 0; + while(k < N) { + + if(!(MIPI_IS_LONG_PACKET(packet[k].header))) { + + packet_timing_t* pkt = &packet[k]; + + unsigned is_long = MIPI_IS_LONG_PACKET(pkt->header)? 1:0; + unsigned data_type = MIPI_GET_DATA_TYPE(pkt->header); + unsigned word_count = MIPI_GET_WORD_COUNT(pkt->header); + + double start_time_us = 0.01 * pkt->start_time; + double end_time_us = 0.01 * pkt->end_time; + double duration_us = 0; + double gap_time_us = start_time_us - prev_end_time_us; + + if(data_type == MIPI_DT_FRAME_START) sprintf(str_extra, "Frame Start" ); + else if(data_type == MIPI_DT_FRAME_END) sprintf(str_extra, "Frame End" ); + else sprintf(str_extra, "Unknown Short Packet"); + + + + printf(FMT, k, data_type, word_count, gap_time_us, start_time_us, duration_us, end_time_us, 1, str_extra); + + prev_end_time_us = start_time_us; + k++; + } else { + + // It's a long packet. + // Aggregate packets until we encounter one with a different + // (data type, payload size) than the current one. + + int i = k + 1; + while((i +#include +#include +#include "i2c.h" +#include "app.h" + +// I2C interface ports +on tile[0]: port p_scl = XS1_PORT_1N; +on tile[0]: port p_sda = XS1_PORT_1O; + +// astew: TIL xscope_user_init() is an XC magic function that gets called +// automatically..for some reason. + +void xscope_user_init() { + xscope_register(0, 0, "", 0, ""); + xscope_config_io(XSCOPE_IO_BASIC); +} + +int main(void) +{ + i2c_master_if i2c[1]; + par { + on tile[0]: i2c_master(i2c, 1, p_scl, p_sda, 400); + on tile[MIPI_TILE]: mipi_main(i2c[0]); + + } + return 0; +} From 3412ecfe929e4caaac829e70ba33630c263af295 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Thu, 8 Jun 2023 11:16:09 +0100 Subject: [PATCH 051/306] review comments --- examples/take_picture_raw/src/app_raw.c | 3 +-- python/run_xscope_bin.py | 3 ++- utils/io_utils/io_utils.c | 14 +++++++++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index 1aadd36c..a79523cd 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -25,7 +25,6 @@ void user_app_raw(streaming_chanend_t c_cam_api){ printf("Image captured...\n"); // Save the image to a file - size_t img_size = MIPI_IMAGE_HEIGHT_PIXELS * MIPI_LINE_WIDTH_BYTES * sizeof(uint8_t); - write_image_file("capture.bin", (uint8_t * ) &image_buffer[0], img_size, MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES); + write_image_file("capture.bin", (uint8_t * ) &image_buffer[0], MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES, 1); exit(0); } diff --git a/python/run_xscope_bin.py b/python/run_xscope_bin.py index a8c9e76f..0bbf6f1c 100644 --- a/python/run_xscope_bin.py +++ b/python/run_xscope_bin.py @@ -25,6 +25,7 @@ def get_adapter_id(): for i, expected_line in enumerate(expected_header): if xrun_out[i] != expected_line: header_match = False + break if not header_match: raise RuntimeError( @@ -35,7 +36,7 @@ def get_adapter_id(): try: if "No Available Devices Found" in xrun_out[4]: raise RuntimeError(f"Error: No available devices found\n") - return + except IndexError: raise RuntimeError(f"Error: xrun output is too short:\n{xrun_out}\n") diff --git a/utils/io_utils/io_utils.c b/utils/io_utils/io_utils.c index 31940839..eb7e26d8 100644 --- a/utils/io_utils/io_utils.c +++ b/utils/io_utils/io_utils.c @@ -2,14 +2,22 @@ #include "io_utils.h" #include -void write_image_file(char * filename, uint8_t * image, const size_t size, const size_t height, const size_t width) +void write_file(char * filename, uint8_t * data, const size_t size) { xscope_file_t fp = xscope_open_file(filename, "wb"); - printf("Writing image...\n"); - xscope_fwrite(&fp, image, size); + xscope_fwrite(&fp, data, size); xscope_close_all_files(); +} + +void write_image_file(char * filename, uint8_t * image, const size_t height, const size_t width, const size_t channels) +{ + printf("Writing image...\n"); + + const size_t img_size = height * width * channels * sizeof(uint8_t); + write_file(filename, image, img_size); + printf("Image written into file: %s\n", filename); printf("Image dimentions: %d x %d\n", width, height); } From 9724bec84d50433648f9dadcc1a715be4c127a2f Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Thu, 8 Jun 2023 15:10:43 +0100 Subject: [PATCH 052/306] moving app_timing to test --- tests/hardware_tests/test_timing/CMakeLists.txt | 4 ++-- tests/hardware_tests/test_timing/src/app.h | 3 ++- tests/hardware_tests/test_timing/src/app.xc | 16 +++++++++++++--- utils/io_utils/io_utils.h | 8 +++++++- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/tests/hardware_tests/test_timing/CMakeLists.txt b/tests/hardware_tests/test_timing/CMakeLists.txt index b2ddfad4..ee246f59 100644 --- a/tests/hardware_tests/test_timing/CMakeLists.txt +++ b/tests/hardware_tests/test_timing/CMakeLists.txt @@ -44,7 +44,7 @@ set(APP_COMPILE_DEFINITIONS set(APP_LINK_OPTIONS "-report" "-target=${XCORE_TARGET}" - "${CONFIG_XSCOPE_PATH}/config.xscope" + "${CONFIG_XSCOPE_PATH_TEST}/config.xscope" ) @@ -53,7 +53,7 @@ set(APP_COMMON_LINK_LIBRARIES mipi::lib_mipi i2c::lib_i2c sensors::lib_imx - camera::lib_camera + camera::lib_camera ) diff --git a/tests/hardware_tests/test_timing/src/app.h b/tests/hardware_tests/test_timing/src/app.h index 80a8e8e4..5f0563ef 100644 --- a/tests/hardware_tests/test_timing/src/app.h +++ b/tests/hardware_tests/test_timing/src/app.h @@ -4,7 +4,8 @@ #include #include "mipi.h" -#include "camera.h" + +#define MIPI_TILE 1 #ifndef MIPI_CLKBLK # define MIPI_CLKBLK XS1_CLKBLK_1 diff --git a/tests/hardware_tests/test_timing/src/app.xc b/tests/hardware_tests/test_timing/src/app.xc index d67cbb29..8515b16e 100644 --- a/tests/hardware_tests/test_timing/src/app.xc +++ b/tests/hardware_tests/test_timing/src/app.xc @@ -21,13 +21,11 @@ #include #include "i2c.h" -#include "app.h" // Sensor #define MSG_SUCCESS "Stream start OK\n" #define MSG_FAIL "Stream start Failed\n" - //////////////////////////////////////////////////////////////// // if true, print warnings when unexpected packet sequences are observed #ifndef SEQUENCE_WARNINGS @@ -72,6 +70,12 @@ #define TABLE_ROWS (12020) + +#define CSV_FORMATTING "0x%08X,0x%08X,0x%08X\n" +#define CSV_FORMATTING_LEN 33 +#define CSV_HEADER "HEADER,START,END\n" +#define CSV_HEADER_LEN 17 + //////////////////////////////////////////////////////////////// // Start port declarations @@ -380,8 +384,11 @@ void writePacketLog( return; } + // WRITE HEADER + fwrite(CSV_HEADER, CSV_HEADER_LEN, 1, log_file); + for(int k = 0; k < N; k++){ - fprintf(log_file, "0x%08X,0x%08X,0x%08X\n", + fprintf(log_file, CSV_FORMATTING, packet[k].header, packet[k].start_time, packet[k].end_time); } @@ -389,6 +396,8 @@ void writePacketLog( } + + static inline unsigned can_aggregate(mipi_header_t a, mipi_header_t b) { @@ -549,6 +558,7 @@ void mipi_main( printf("Writing packet log to %s..\n", PACKET_LOG_FILE); writePacketLog(PACKET_LOG_FILE, packet_log, TABLE_ROWS); printf(" ...done.\n\n"); + } exit(0); diff --git a/utils/io_utils/io_utils.h b/utils/io_utils/io_utils.h index cf6a0c49..4b2b018d 100644 --- a/utils/io_utils/io_utils.h +++ b/utils/io_utils/io_utils.h @@ -10,7 +10,13 @@ extern "C" { #include "xscope_io_device.h" -void write_image_file(char * filename, uint8_t * image, const size_t size, const size_t height, const size_t width); +void write_file(char * filename, uint8_t * data, const size_t size); +void write_image_file( + char * filename, + uint8_t * image, + const size_t size, + const size_t height, + const size_t width); #ifdef __XC__ } From 5a4bd501c82cc6f06c45a4679305d5961fa7e07b Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Thu, 8 Jun 2023 16:43:32 +0100 Subject: [PATCH 053/306] change to DEFAULT_MIPI_DPHY_CFG3 --- camera/src/camera.xc | 4 ++-- modules/mipi/api/mipi_defines.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/camera/src/camera.xc b/camera/src/camera.xc index 105a4d19..d3ab7b3e 100644 --- a/camera/src/camera.xc +++ b/camera/src/camera.xc @@ -40,7 +40,7 @@ void camera_main( // Assigning lanes and polarities write_node_config_reg(mipi_tile, XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, - DEFAULT_MIPI_SHIM_CFG3); + DEFAULT_MIPI_DPHY_CFG3); // send packet to MIPI shim MipiPacketRx_init(mipi_tile, @@ -99,7 +99,7 @@ void camera_main_raw( // in the explorer BOARD DPDN is swap write_node_config_reg(mipi_tile, XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, - DEFAULT_MIPI_SHIM_CFG3); //TODO decompose into different values + DEFAULT_MIPI_DPHY_CFG3); //TODO decompose into different values // send packet to MIPI shim MipiPacketRx_init(mipi_tile, diff --git a/modules/mipi/api/mipi_defines.h b/modules/mipi/api/mipi_defines.h index cb0b5481..ed454087 100644 --- a/modules/mipi/api/mipi_defines.h +++ b/modules/mipi/api/mipi_defines.h @@ -136,7 +136,7 @@ typedef enum xMIPI_DemuxMode_t { #define _LANE_SWAP_LAN0_DEFAULT (0 << 3) #define _LANE_SWAP_CLK_DEFAULT (2 << 0) -#define DEFAULT_MIPI_SHIM_CFG3 ( \ +#define DEFAULT_MIPI_DPHY_CFG3 ( \ _ENABLE_LAN1 |\ _ENABLE_LAN0 |\ _ENABLE_CLK |\ From 0aac8478341e031e82a6701fe3d46d72e19c9b5d Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Thu, 8 Jun 2023 17:13:14 +0100 Subject: [PATCH 054/306] cmake cleanup --- .../hardware_tests/test_timing/CMakeLists.txt | 50 ++++--------------- 1 file changed, 9 insertions(+), 41 deletions(-) diff --git a/tests/hardware_tests/test_timing/CMakeLists.txt b/tests/hardware_tests/test_timing/CMakeLists.txt index ee246f59..1c6a638b 100644 --- a/tests/hardware_tests/test_timing/CMakeLists.txt +++ b/tests/hardware_tests/test_timing/CMakeLists.txt @@ -1,69 +1,37 @@ -#********************** -# Gather Sources -#********************** - -# <--- Set the executable +# ############################################################################## set(TARGET test_timing) -set(APP_INCLUDES - ${CMAKE_CURRENT_LIST_DIR}/src -) - -file(GLOB_RECURSE APP_SOURCES - ${CMAKE_CURRENT_LIST_DIR}/src/*.c - ${CMAKE_CURRENT_LIST_DIR}/src/*.xc - ${CMAKE_CURRENT_LIST_DIR}/src/*.S +set(SOURCES + src/app.xc + src/main.xc + src/MipiGatherTiming.S ) -#********************** -# Flags -#********************** set(APP_COMPILER_FLAGS -Os -g -report -fxscope - -mcmodel=large - -Wno-xcore-fptrgroup -target=${XCORE_TARGET} ${CONFIG_XSCOPE_PATH_TEST}/config.xscope ) - -set(APP_COMPILE_DEFINITIONS - configENABLE_DEBUG_PRINTF=1 - PLATFORM_SUPPORTS_TILE_0=1 - PLATFORM_SUPPORTS_TILE_1=1 - PLATFORM_SUPPORTS_TILE_2=0 - PLATFORM_SUPPORTS_TILE_3=0 - PLATFORM_USES_TILE_0=1 - PLATFORM_USES_TILE_1=1 - XUD_CORE_CLOCK=600 -) - set(APP_LINK_OPTIONS "-report" "-target=${XCORE_TARGET}" "${CONFIG_XSCOPE_PATH_TEST}/config.xscope" ) - -# <--- Link libraries set(APP_COMMON_LINK_LIBRARIES mipi::lib_mipi i2c::lib_i2c sensors::lib_imx camera::lib_camera - ) - +) -#********************** -# Tile Targets -#********************** -add_executable(${TARGET} EXCLUDE_FROM_ALL) -target_sources(${TARGET} PUBLIC ${APP_SOURCES}) -target_include_directories(${TARGET} PUBLIC ${APP_INCLUDES}) -target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) +# ############################################################################## +add_executable(${TARGET}) +target_sources(${TARGET} PUBLIC ${SOURCES}) target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) From 64262e7645b24c6fdf1d982e60a0d436b1308763 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Fri, 9 Jun 2023 12:05:28 +0100 Subject: [PATCH 055/306] adding dimention swap, changing xscope_fileio pin --- examples/take_picture_downsample/CMakeLists.txt | 6 ++++-- examples/take_picture_downsample/src/app.c | 13 ++++++++----- examples/take_picture_raw/CMakeLists.txt | 4 ++-- utils/io_utils/io_utils.c | 17 +++++++++++++++++ utils/io_utils/io_utils.h | 4 +++- utils/xscope_fileio | 2 +- 6 files changed, 35 insertions(+), 11 deletions(-) diff --git a/examples/take_picture_downsample/CMakeLists.txt b/examples/take_picture_downsample/CMakeLists.txt index d2eeca56..112729b5 100644 --- a/examples/take_picture_downsample/CMakeLists.txt +++ b/examples/take_picture_downsample/CMakeLists.txt @@ -42,8 +42,10 @@ set(APP_LINK_OPTIONS set(APP_COMMON_LINK_LIBRARIES mipi::lib_mipi i2c::lib_i2c - sensors::lib_imx - camera::lib_camera + sensors::lib_imx + camera::lib_camera + fwk_camera::utils + fwk_camera::xscope_fileio ) diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index 54476669..42daa6a6 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -3,10 +3,11 @@ #include #include +#include "io_utils.h" void user_app(streaming_chanend_t c_cam_api){ int8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; - uint8_t out_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; + uint8_t temp_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; // set the input image to 0 memset(image_buffer, -128, APP_IMAGE_CHANNEL_COUNT * APP_IMAGE_HEIGHT_PIXELS * APP_IMAGE_WIDTH_PIXELS); @@ -20,14 +21,16 @@ void user_app(streaming_chanend_t c_cam_api){ // Rotate 180 degrees // rotate_image(image_buffer); - // convert to uint8 - img_int8_to_uint8(image_buffer, out_buffer); + // convert to uint8 with right dimentions + img_int8_to_uint8(image_buffer, temp_buffer); + uint8_t * io_buff = (uint8_t *) &image_buffer[0][0][0]; // io_buff this will have [APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS][APP_IMAGE_CHANNEL_COUNT] + swap_dimentions((uint8_t *) &temp_buffer[0][0][0], io_buff, APP_IMAGE_HEIGHT_PIXELS, APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_CHANNEL_COUNT); // Write binary file and .bmp file - write_image("capture.bin", out_buffer); + write_image_file("capture.bin", io_buff, APP_IMAGE_HEIGHT_PIXELS, APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_CHANNEL_COUNT); //save it to bmp - writeBMP("capture.bmp", out_buffer); + writeBMP("capture.bmp", temp_buffer); // end here exit(0); diff --git a/examples/take_picture_raw/CMakeLists.txt b/examples/take_picture_raw/CMakeLists.txt index acf48a4d..db596546 100644 --- a/examples/take_picture_raw/CMakeLists.txt +++ b/examples/take_picture_raw/CMakeLists.txt @@ -42,8 +42,8 @@ set(APP_LINK_OPTIONS set(APP_COMMON_LINK_LIBRARIES mipi::lib_mipi i2c::lib_i2c - sensors::lib_imx - camera::lib_camera + sensors::lib_imx + camera::lib_camera fwk_camera::utils fwk_camera::xscope_fileio ) diff --git a/utils/io_utils/io_utils.c b/utils/io_utils/io_utils.c index eb7e26d8..c0935bf2 100644 --- a/utils/io_utils/io_utils.c +++ b/utils/io_utils/io_utils.c @@ -2,6 +2,23 @@ #include "io_utils.h" #include +void swap_dimentions(uint8_t * image_in, uint8_t * image_out, const size_t height, const size_t width, const size_t channels) +{ + printf("Swapping image dimentions\n"); + for(size_t k = 0; k < height; k++) + { + for(size_t j = 0; j < width; j++) + { + for(size_t c = 0; c < channels; c++) + { + size_t index_in = c * (height * width) + k * width + j - 1; + size_t index_out = k * (width * channels) + j * channels + c - 1; + image_out[index_out] = image_in[index_in]; + } + } + } +} + void write_file(char * filename, uint8_t * data, const size_t size) { xscope_file_t fp = xscope_open_file(filename, "wb"); diff --git a/utils/io_utils/io_utils.h b/utils/io_utils/io_utils.h index cf6a0c49..fe365d30 100644 --- a/utils/io_utils/io_utils.h +++ b/utils/io_utils/io_utils.h @@ -10,7 +10,9 @@ extern "C" { #include "xscope_io_device.h" -void write_image_file(char * filename, uint8_t * image, const size_t size, const size_t height, const size_t width); +void swap_dimentions(uint8_t * image_in, uint8_t * image_out, const size_t height, const size_t width, const size_t channels); + +void write_image_file(char * filename, uint8_t * image, const size_t height, const size_t width, const size_t channels); #ifdef __XC__ } diff --git a/utils/xscope_fileio b/utils/xscope_fileio index 52cff082..cca94745 160000 --- a/utils/xscope_fileio +++ b/utils/xscope_fileio @@ -1 +1 @@ -Subproject commit 52cff0826b2773beec49044a0729bb000c011379 +Subproject commit cca94745b63b3f7bbc93fc34f2111a196a8937df From 44308e2456ac6dd88591b65b6a2fd027e3684f62 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Fri, 9 Jun 2023 12:48:30 +0100 Subject: [PATCH 056/306] first doc draft, camera naming change --- camera/api/{user_api.h => camera_api.h} | 0 camera/api/{camera.h => camera_main.h} | 2 +- camera/api/packet_handler.h | 2 +- camera/src/{user_api.c => camera_api.c} | 2 +- camera/src/{camera.xc => camera_main.xc} | 2 +- camera/src/packet_handler.c | 2 +- doc/programming_guide/01_Introduction.rst | 47 +++++++++++++++++++ .../02_Architecture_and_Design.rst | 25 ++++++++++ doc/programming_guide/index.rst | 13 +++++ examples/take_picture_downsample/src/app.h | 2 +- examples/take_picture_downsample/src/main.xc | 2 +- examples/take_picture_raw/src/app_raw.c | 2 +- examples/take_picture_raw/src/app_raw.h | 2 +- examples/take_picture_raw/src/main.xc | 2 +- 14 files changed, 95 insertions(+), 10 deletions(-) rename camera/api/{user_api.h => camera_api.h} (100%) rename camera/api/{camera.h => camera_main.h} (98%) rename camera/src/{user_api.c => camera_api.c} (98%) rename camera/src/{camera.xc => camera_main.xc} (99%) create mode 100644 doc/programming_guide/01_Introduction.rst create mode 100644 doc/programming_guide/02_Architecture_and_Design.rst create mode 100644 doc/programming_guide/index.rst diff --git a/camera/api/user_api.h b/camera/api/camera_api.h similarity index 100% rename from camera/api/user_api.h rename to camera/api/camera_api.h diff --git a/camera/api/camera.h b/camera/api/camera_main.h similarity index 98% rename from camera/api/camera.h rename to camera/api/camera_main.h index 8895544f..69a84228 100644 --- a/camera/api/camera.h +++ b/camera/api/camera_main.h @@ -20,7 +20,7 @@ #include "image_vfilter.h" #include "packet_handler.h" #include "statistics.h" -#include "user_api.h" +#include "camera_api.h" #include "utils.h" diff --git a/camera/api/packet_handler.h b/camera/api/packet_handler.h index 1b874515..9956ee93 100644 --- a/camera/api/packet_handler.h +++ b/camera/api/packet_handler.h @@ -6,7 +6,7 @@ #include "xccompat.h" -#include "camera.h" +#include "camera_main.h" #ifdef __XC__ extern "C" { diff --git a/camera/src/user_api.c b/camera/src/camera_api.c similarity index 98% rename from camera/src/user_api.c rename to camera/src/camera_api.c index 5833a81c..bbd59125 100644 --- a/camera/src/user_api.c +++ b/camera/src/camera_api.c @@ -6,7 +6,7 @@ // user #include "mipi.h" #include "utils.h" -#include "user_api.h" +#include "camera_api.h" // Pointers to both downsampled or raw static image_t *user_image; diff --git a/camera/src/camera.xc b/camera/src/camera_main.xc similarity index 99% rename from camera/src/camera.xc rename to camera/src/camera_main.xc index 105a4d19..f07af7d2 100644 --- a/camera/src/camera.xc +++ b/camera/src/camera_main.xc @@ -9,7 +9,7 @@ #include #include "i2c.h" -#include "camera.h" +#include "camera_main.h" #include "mipi_defines.h" #include "packet_handler.h" #include "statistics.h" diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index 86c791ac..a03a4389 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -8,7 +8,7 @@ #include "packet_handler.h" #include "image_vfilter.h" #include "image_hfilter.h" -#include "user_api.h" +#include "camera_api.h" #include "utils.h" #include "sensor.h" diff --git a/doc/programming_guide/01_Introduction.rst b/doc/programming_guide/01_Introduction.rst new file mode 100644 index 00000000..426c4fd9 --- /dev/null +++ b/doc/programming_guide/01_Introduction.rst @@ -0,0 +1,47 @@ +Introduction +============= + +.. contents:: Table of Contents + +Overview +--------- +The purpose of this programming guide is to provide developers with a comprehensive understanding of the FWK_Camera architecture and guide them on how to effectively interact with camera using xmos devices. + +The architecture consists of several key components that work together to facilitate camera communication and data processing. +These components include: + +#. Camera hardware and camera interface +#. Camera drivers +#. User aspplication / user interface +#. Image signal processing +#. I/O + +[INSERT HERE HIGH LEVEL DIAGRAM] + +Conventions and Terminology +--------------------------- +(MIPI definitions and specs) + +(xcore spifications channends, for example) + +(I2C control, ) + + +Features +--------- +* MIPI CSI2 interface +* Up to 1GBps per lane +* Low resolution filtering +* Cameras supported: + * IMX219 + * GC2145 (explain hw modification) + +Getting Started +---------------- + + +Additional Resources +--------------------- +(MIPi-csi specs) +(mipi xcore reference) +(camera datahseets) diff --git a/doc/programming_guide/02_Architecture_and_Design.rst b/doc/programming_guide/02_Architecture_and_Design.rst new file mode 100644 index 00000000..269c54f2 --- /dev/null +++ b/doc/programming_guide/02_Architecture_and_Design.rst @@ -0,0 +1,25 @@ +Architecture and Design +======================= + +.. contents:: Table of Contents + +Introduction +------------- + +System Architecture +-------------------- + +.. image:: images/overview.drawio.png + :alt: System architecture + :width: 300 + :height: 200 + + +Module Descriptions +--------------------- + +Optimizations and Future Directions +------------------------------------ + +Conclusion +------------ diff --git a/doc/programming_guide/index.rst b/doc/programming_guide/index.rst new file mode 100644 index 00000000..eaf1e0ff --- /dev/null +++ b/doc/programming_guide/index.rst @@ -0,0 +1,13 @@ +.. Fwk camera Programming Guide Documentation + +.. toctree:: + :maxdepth: 2 + + 01_Introduction + 02_Architecture_and_Design + 03_Building_the_Software + 04_Testing_the_Software + 05_Modifying_the_Software + 06_Adding_New_Cameras + 07_Examples + 08_Troubleshooting diff --git a/examples/take_picture_downsample/src/app.h b/examples/take_picture_downsample/src/app.h index d1a3078b..057c72d3 100644 --- a/examples/take_picture_downsample/src/app.h +++ b/examples/take_picture_downsample/src/app.h @@ -2,6 +2,6 @@ #include "platform.h" #include "xccompat.h" -#include "camera.h" +#include "camera_main.h" void user_app(streaming_chanend_t c_cam_api); diff --git a/examples/take_picture_downsample/src/main.xc b/examples/take_picture_downsample/src/main.xc index be13aa72..1a8758f9 100644 --- a/examples/take_picture_downsample/src/main.xc +++ b/examples/take_picture_downsample/src/main.xc @@ -4,7 +4,7 @@ #include #include "i2c.h" -#include "camera.h" +#include "camera_main.h" #include "app.h" // I2C interface ports diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index c20bf9dc..be6bc211 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -6,7 +6,7 @@ // user #include "mipi.h" //#include "utils.h" -#include "user_api.h" +#include "camera_api.h" #include "app_raw.h" #include "io_utils.h" diff --git a/examples/take_picture_raw/src/app_raw.h b/examples/take_picture_raw/src/app_raw.h index 0ccad37e..86a2ed6b 100644 --- a/examples/take_picture_raw/src/app_raw.h +++ b/examples/take_picture_raw/src/app_raw.h @@ -2,6 +2,6 @@ #include "platform.h" #include "xccompat.h" -#include "camera.h" +#include "camera_main.h" void user_app_raw(streaming_chanend_t c_cam_api); diff --git a/examples/take_picture_raw/src/main.xc b/examples/take_picture_raw/src/main.xc index ca3afa7a..ee28570e 100644 --- a/examples/take_picture_raw/src/main.xc +++ b/examples/take_picture_raw/src/main.xc @@ -4,7 +4,7 @@ #include #include "i2c.h" -#include "camera.h" +#include "camera_main.h" #include "app_raw.h" // I2C interface ports From 28426a1250c326af21de1c9e91e5427e829e5088 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Fri, 9 Jun 2023 12:56:44 +0100 Subject: [PATCH 057/306] index change --- doc/programming_guide/02_Architecture_and_Design.rst | 11 +++++++---- doc/programming_guide/index.rst | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/doc/programming_guide/02_Architecture_and_Design.rst b/doc/programming_guide/02_Architecture_and_Design.rst index 269c54f2..da8ee871 100644 --- a/doc/programming_guide/02_Architecture_and_Design.rst +++ b/doc/programming_guide/02_Architecture_and_Design.rst @@ -9,10 +9,13 @@ Introduction System Architecture -------------------- -.. image:: images/overview.drawio.png - :alt: System architecture - :width: 300 - :height: 200 +.. figure:: images/overviewdrawio.png + :alt: Alternative Text + :figwidth: 400px + :figclass: custom-class + + Caption for the Figure. + Module Descriptions diff --git a/doc/programming_guide/index.rst b/doc/programming_guide/index.rst index eaf1e0ff..4431e39d 100644 --- a/doc/programming_guide/index.rst +++ b/doc/programming_guide/index.rst @@ -1,4 +1,4 @@ -.. Fwk camera Programming Guide Documentation +Fwk camera Programming Guide Documentation .. toctree:: :maxdepth: 2 From e9206ca31f19b5ccd83943cf14a50bdc08e7b5bc Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Fri, 9 Jun 2023 13:00:26 +0100 Subject: [PATCH 058/306] giginore change --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 16491c1d..ed70e5f0 100644 --- a/.gitignore +++ b/.gitignore @@ -83,4 +83,7 @@ temp # dotenv .env -**/*.png \ No newline at end of file +**/*.png + +#documentation +!doc/images/*.png From 2fc4366a919e3166c1b998374b9880096ab4a9cc Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Fri, 9 Jun 2023 13:06:31 +0100 Subject: [PATCH 059/306] gitignore update --- .gitignore | 8 +++++--- doc/programming_guide/images/overviewdrawio.png | Bin 0 -> 99008 bytes 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 doc/programming_guide/images/overviewdrawio.png diff --git a/.gitignore b/.gitignore index ed70e5f0..423025a6 100644 --- a/.gitignore +++ b/.gitignore @@ -78,12 +78,14 @@ tools/ai/ai_tools # temp folder temp - !CMakeLists.txt +# images +*.png + # dotenv .env -**/*.png #documentation -!doc/images/*.png +!**/images/*.png + diff --git a/doc/programming_guide/images/overviewdrawio.png b/doc/programming_guide/images/overviewdrawio.png new file mode 100644 index 0000000000000000000000000000000000000000..aa3fb8216d4df80ae38de42814233f752e79b8dd GIT binary patch literal 99008 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

q+mZfx9p3-@>p1Vj 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 QAAo-+RdrQLm2ATQ3!$l?r2qf` literal 0 HcmV?d00001 From dac7678beb9b90b42914d85f8a8f0376fd469a92 Mon Sep 17 00:00:00 2001 From: Aaron Stewart Date: Fri, 9 Jun 2023 08:40:52 -0400 Subject: [PATCH 060/306] Adds unit tests for the hfilter --- camera/api/camera.h | 19 +++ camera/api/image_hfilter.h | 65 ++++++--- camera/api/packet_handler.h | 23 +++ camera/api/utils.h | 4 +- camera/src/image_hfilter.c | 32 +---- camera/src/packet_handler.c | 54 ++++--- camera/src/utils.c | 7 + tests/unit_tests/src/main.c | 2 +- ...ge_hfilter_test.c => pixel_hfilter_test.c} | 136 ++++++++++-------- 9 files changed, 217 insertions(+), 125 deletions(-) rename tests/unit_tests/src/test/{image_hfilter_test.c => pixel_hfilter_test.c} (73%) diff --git a/camera/api/camera.h b/camera/api/camera.h index c672d184..bb9added 100644 --- a/camera/api/camera.h +++ b/camera/api/camera.h @@ -37,6 +37,20 @@ #ifdef __XC__ +/** + * Thread entry point for interfacing with the camera sensor. + * + * This version of the camera thread will perform decimation and demosaicing. + * + * @param mipi_tile The tile on which the MIPI receiver is located + * @param p_mipi_clk The MIPI clock input port + * @param p_mipi_rxa The MIPI active input port + * @param p_mipi_rxv The MIPI valid input port + * @param p_mipi_rxd The MIPI data input port + * @param clk_mipi The MIPI clock block + * @param i2c The I2C client interface + * @param c_user_api The user API channel + */ void camera_main( tileref mipi_tile, in port p_mipi_clk, @@ -47,6 +61,11 @@ void camera_main( client interface i2c_master_if i2c, streaming chanend c_user_api); +/** + * Thread entry point for interfacing with the camera sensor. + * + * This version of the camera thread will capture raw data. + */ void camera_main_raw( tileref mipi_tile, in port p_mipi_clk, diff --git a/camera/api/image_hfilter.h b/camera/api/image_hfilter.h index 3179d1eb..1f68b646 100644 --- a/camera/api/image_hfilter.h +++ b/camera/api/image_hfilter.h @@ -9,11 +9,44 @@ extern "C" { #endif - +// The FIR filter used for horizontal filtering has 3 taps, and is +// (0.20872991, 0.58254019, 0.20872991) +// Because of symmetry we can use a single coefficient for the even taps. +// The filter coefficients are scaled by a gain derived from the white balance +// algorithm, and then scaled to an appropriate fixed-point format. #define COEF_B0 (0.58254019f) #define COEF_B1 (0.20872991f) +/** + * This structure holds the state for the horizontal filter. + */ +typedef struct { + /// @brief The initial value for the accumulator + int32_t acc_init; + /// @brief The filter coefficients + int8_t coef[32]; + /// @brief The shift applied to the accumulator to get the output + unsigned shift; +} hfilter_state_t; + +/** + * This function performs a horizontal filtering operation on a single row + * of pixels. + * + * The input and output arrays are assumed to be aligned to word boundaries. + * + * The output pixels will be adjacent in memory, so this function is used to + * split an image with interleaved color planes into separate color planes. + * + * @param output The output array of pixels + * @param input The input array of pixels + * @param coef The filter coefficients + * @param acc_init The initial value for the accumulator + * @param shift The shift applied to the accumulator to get the output + * @param input_stride The number of input pixels to skip between output pixels + * @param output_count The number of output pixels to generate + */ void pixel_hfilter( int8_t output[], const int8_t input[], @@ -23,26 +56,26 @@ void pixel_hfilter( const int32_t input_stride, const unsigned output_count); -typedef struct { - int32_t acc_init; - int8_t coef[32]; - unsigned shift; -} hfilter_state_t; - -void image_hfilter_update_scale( +/** + * This function is used to update the filter parameters based on a gain to be + * applied to the color channel represented by `state`. + * + * `offset` is the byte offset (from the beginning of a row of pixels) of the + * first pixel in the color channel represented by `state`. So, for a Bayered + * RGGB image, the offset for the red channel would be 0, and the offset for + * the blue channel would be 1. The offset for the green channel depends on + * whether the red-adjascent or blue-adjascent green pixel is used. + * + * @param state The filter state to update + * @param gain The gain to apply to the filter coefficients + * @param offset The offset into the filter coefficient array to start at. + */ +void pixel_hfilter_update_scale( hfilter_state_t* state, const float gain, const unsigned offset); -// astew: I'm confused about this function now...this is in the camera -// code, but it's referencing macros that only make sense in the -// context of an application, because applications should be able to -// have different APP_IMAGE_WIDTH_PIXELS settings.. -void image_hfilter( - int8_t pix_out[APP_IMAGE_WIDTH_PIXELS], - const hfilter_state_t* state, - const int8_t pix_in[SENSOR_RAW_IMAGE_WIDTH_PIXELS]); #if defined(__XC__) || defined(__cplusplus) diff --git a/camera/api/packet_handler.h b/camera/api/packet_handler.h index 1b874515..9c53b048 100644 --- a/camera/api/packet_handler.h +++ b/camera/api/packet_handler.h @@ -12,6 +12,9 @@ extern "C" { #endif +/** + * Represents a received MIPI packet. + */ typedef struct { mipi_header_t header; @@ -19,12 +22,32 @@ typedef struct } mipi_packet_t; +// struct to hold the calculated parameters for the ISP +typedef struct { + float channel_gain[APP_IMAGE_CHANNEL_COUNT]; +} isp_params_t; + + +/** + * isp_params_t instance owned by the packet handler. + */ +extern +isp_params_t isp_params; + + +/** + * Thread entry point for packet handling when decimation and demosaicing are + * desired. + */ void mipi_packet_handler( streaming_chanend_t c_pkt, streaming_chanend_t c_ctrl, streaming_chanend_t c_out_row, streaming_chanend_t c_user_api); +/** + * Thread entry point for packet handling when capturing raw data. + */ void mipi_packet_handler_raw( streaming_chanend_t c_pkt, streaming_chanend_t c_ctrl, diff --git a/camera/api/utils.h b/camera/api/utils.h index f3eae490..aa72f617 100644 --- a/camera/api/utils.h +++ b/camera/api/utils.h @@ -12,8 +12,8 @@ extern "C" { #endif static inline -int measure_time(){ - int y = 0; +unsigned measure_time(){ + unsigned y = 0; asm volatile("gettime %0": "=r"(y)); return y; } diff --git a/camera/src/image_hfilter.c b/camera/src/image_hfilter.c index 6f5a6c35..023434b6 100644 --- a/camera/src/image_hfilter.c +++ b/camera/src/image_hfilter.c @@ -6,26 +6,11 @@ #include "image_hfilter.h" #include "isp.h" -// The filter coefficients for the horizontal filter. Because the VPU (and in -// particular, VLMACCR) is used, this needs to be 32 bytes long, padded with -// zeros where necessary. - -// VPU loads must be 4-byte-aligned. So when filtering e.g. a bayered RGGB -// image we can't filter red, move up one byte in the pixel row, and apply the -// same filter again. Instead we need a pair of filters, containing the same -// coefficients, but offset by one index. Then, when horizontally filtering the -// G channel we provide the filter function the SAME (word-aligned) start -// address that we provided with the R channel, but using the offset -// coefficients so that the R channel is ignored instead of the G channel. -// Additionally, note that to avoid mixing channels, every other coefficient -// must be zero, in each case. -#define HFILTER_INPUT_STRIDE (APP_DECIMATION_FACTOR) //Note: for filter coefficients reference : python/filters.txt - -void image_hfilter_update_scale( +void pixel_hfilter_update_scale( hfilter_state_t* state, const float gain, const unsigned offset) @@ -58,18 +43,3 @@ void image_hfilter_update_scale( state->acc_init = (128 * (sum_b - shift_scale)); } - - -void image_hfilter( - int8_t pix_out[APP_IMAGE_WIDTH_PIXELS], - const hfilter_state_t* state, - const int8_t pix_in[SENSOR_RAW_IMAGE_WIDTH_PIXELS]) -{ - pixel_hfilter(&pix_out[0], - &pix_in[0], - &state->coef[0], - state->acc_init, - state->shift, - HFILTER_INPUT_STRIDE, - APP_IMAGE_WIDTH_PIXELS); -} diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index fdce8843..2bb54f30 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -36,13 +36,18 @@ hfilter_state_t hfilter_state[APP_IMAGE_CHANNEL_COUNT]; // Initial channel scales #define AWB_gain_RED 1.3 -#define AWB_gain_BLUE 0.8 #define AWB_gain_GREEN 1.3 - -float channel_scales[APP_IMAGE_CHANNEL_COUNT] = { - AWB_gain_RED, - AWB_gain_GREEN, - AWB_gain_BLUE +#define AWB_gain_BLUE 0.8 +// #define AWB_gain_RED 1.0 +// #define AWB_gain_GREEN 1.0 +// #define AWB_gain_BLUE 1.0 + +isp_params_t isp_params = { + .channel_gain = { + AWB_gain_RED, + AWB_gain_GREEN, + AWB_gain_BLUE + } }; @@ -52,8 +57,10 @@ void handle_frame_start( { // New frame is starting, reset the vertical filter accumulator states. for(int c = 0; c < APP_IMAGE_CHANNEL_COUNT; c++){ - image_hfilter_update_scale(&hfilter_state[c], channel_scales[c], + pixel_hfilter_update_scale(&hfilter_state[c], + isp_params.channel_gain[c], (c == 0)? 0 : 1); + image_vfilter_frame_init(&vfilter_accs[c][0]); } @@ -71,6 +78,8 @@ void handle_unknown_packet( // 2 - error packets (in this case mipi reciever will raise an exception, but in the future we want to handle them here) } +#define HFILTER_INPUT_STRIDE (APP_DECIMATION_FACTOR) + /** * Handle a row of pixel data. * @@ -94,18 +103,27 @@ unsigned handle_pixel_data( if(pattern == 0){ // Packet contains RGRGRGRGRGRGRGRGRG... ////// RED - image_hfilter(&hfilt_row[0], - &hfilter_state[CHAN_RED], - (int8_t*) &pkt->payload[0]); + pixel_hfilter(&hfilt_row[0], + (int8_t*) &pkt->payload[0], + &hfilter_state[CHAN_RED].coef[0], + hfilter_state[CHAN_RED].acc_init, + hfilter_state[CHAN_RED].shift, + HFILTER_INPUT_STRIDE, + APP_IMAGE_WIDTH_PIXELS); + image_vfilter_process_row(&output_buffer[CHAN_RED][0], &vfilter_accs[CHAN_RED][0], &hfilt_row[0]); ////// GREEN - image_hfilter(&hfilt_row[0], - &hfilter_state[CHAN_GREEN], - (int8_t*) &pkt->payload[0]); + pixel_hfilter(&hfilt_row[0], + (int8_t*) &pkt->payload[0], + &hfilter_state[CHAN_GREEN].coef[0], + hfilter_state[CHAN_GREEN].acc_init, + hfilter_state[CHAN_GREEN].shift, + HFILTER_INPUT_STRIDE, + APP_IMAGE_WIDTH_PIXELS); // we now it is not the las row [2] image_vfilter_process_row(&output_buffer[CHAN_GREEN][0], @@ -115,9 +133,13 @@ unsigned handle_pixel_data( } else { // Packet contains GBGBGBGBGBGBGBGBGBGB... ////// BLUE - image_hfilter(&hfilt_row[0], - &hfilter_state[CHAN_BLUE], - (int8_t*) &pkt->payload[0]); + pixel_hfilter(&hfilt_row[0], + (int8_t*) &pkt->payload[0], + &hfilter_state[CHAN_BLUE].coef[0], + hfilter_state[CHAN_BLUE].acc_init, + hfilter_state[CHAN_BLUE].shift, + HFILTER_INPUT_STRIDE, + APP_IMAGE_WIDTH_PIXELS); unsigned new_row = image_vfilter_process_row( &output_buffer[CHAN_BLUE][0], diff --git a/camera/src/utils.c b/camera/src/utils.c index 286c8b3d..a2d131ff 100644 --- a/camera/src/utils.c +++ b/camera/src/utils.c @@ -48,13 +48,20 @@ void write_image_raw( { static FILE* img_file = NULL; img_file = fopen(filename, "wb"); + + uint8_t min = 255; + uint8_t max = 0; + for(uint16_t k = 0; k < MIPI_IMAGE_HEIGHT_PIXELS; k++){ for(uint16_t j = 0; j < MIPI_LINE_WIDTH_BYTES; j++){ uint32_t pos = k * MIPI_LINE_WIDTH_BYTES + j; + min = (image[pos] < min) ? image[pos] : min; + max = (image[pos] > max) ? image[pos] : max; fwrite(&image[pos], sizeof(int8_t), 1, img_file); } } fclose(img_file); + printf("Min %d, Max %d\n", min, max); printf("Outfile %s\n", filename); printf("image size (%dx%d)\n", MIPI_LINE_WIDTH_BYTES, MIPI_IMAGE_HEIGHT_PIXELS); } diff --git a/tests/unit_tests/src/main.c b/tests/unit_tests/src/main.c index 96ad4c16..e3ee6521 100644 --- a/tests/unit_tests/src/main.c +++ b/tests/unit_tests/src/main.c @@ -17,7 +17,7 @@ int main( printf("\n"); - RUN_TEST_GROUP(image_hfilter); + RUN_TEST_GROUP(pixel_hfilter); RUN_TEST_GROUP(isp_tests); return UNITY_END(); diff --git a/tests/unit_tests/src/test/image_hfilter_test.c b/tests/unit_tests/src/test/pixel_hfilter_test.c similarity index 73% rename from tests/unit_tests/src/test/image_hfilter_test.c rename to tests/unit_tests/src/test/pixel_hfilter_test.c index 8e5cfcbb..0c051b38 100644 --- a/tests/unit_tests/src/test/image_hfilter_test.c +++ b/tests/unit_tests/src/test/pixel_hfilter_test.c @@ -12,31 +12,33 @@ #include "camera.h" -TEST_GROUP_RUNNER(image_hfilter) { - RUN_TEST_CASE(image_hfilter, pixel_hfilter__basic); - RUN_TEST_CASE(image_hfilter, pixel_hfilter__acc_init); - RUN_TEST_CASE(image_hfilter, pixel_hfilter__apply_shift); - RUN_TEST_CASE(image_hfilter, pixel_hfilter__input_stride); - RUN_TEST_CASE(image_hfilter, pixel_hfilter__out_count); - RUN_TEST_CASE(image_hfilter, pixel_hfilter__alt_coef); - - RUN_TEST_CASE(image_hfilter, image_hfilter_update_scale__case1); - RUN_TEST_CASE(image_hfilter, image_hfilter_update_scale__case2); - RUN_TEST_CASE(image_hfilter, image_hfilter_update_scale__case3); - RUN_TEST_CASE(image_hfilter, image_hfilter_update_scale__case4); - - // RUN_TEST_CASE(image_hfilter, image_hfilter__case1); +TEST_GROUP_RUNNER(pixel_hfilter) { + RUN_TEST_CASE(pixel_hfilter, pixel_hfilter__basic); + RUN_TEST_CASE(pixel_hfilter, pixel_hfilter__acc_init); + RUN_TEST_CASE(pixel_hfilter, pixel_hfilter__apply_shift); + RUN_TEST_CASE(pixel_hfilter, pixel_hfilter__input_stride); + RUN_TEST_CASE(pixel_hfilter, pixel_hfilter__out_count); + RUN_TEST_CASE(pixel_hfilter, pixel_hfilter__alt_coef); + RUN_TEST_CASE(pixel_hfilter, pixel_hfilter__timing); + + RUN_TEST_CASE(pixel_hfilter, pixel_hfilter_update_scale__case1); + RUN_TEST_CASE(pixel_hfilter, pixel_hfilter_update_scale__case2); + RUN_TEST_CASE(pixel_hfilter, pixel_hfilter_update_scale__case3); + RUN_TEST_CASE(pixel_hfilter, pixel_hfilter_update_scale__case4); + RUN_TEST_CASE(pixel_hfilter, pixel_hfilter_update_scale__timing); + + // RUN_TEST_CASE(pixel_hfilter, pixel_hfilter__case1); } -TEST_GROUP(image_hfilter); -TEST_SETUP(image_hfilter) { fflush(stdout); } -TEST_TEAR_DOWN(image_hfilter) {} +TEST_GROUP(pixel_hfilter); +TEST_SETUP(pixel_hfilter) { fflush(stdout); } +TEST_TEAR_DOWN(pixel_hfilter) {} /////////////////////////////////////////////// /////////////////////////////////////////////// /////////////////////////////////////////////// -TEST(image_hfilter, pixel_hfilter__basic) +TEST(pixel_hfilter, pixel_hfilter__basic) { static const int8_t coef_count = 32; static const unsigned output_count = 16; @@ -65,7 +67,7 @@ TEST(image_hfilter, pixel_hfilter__basic) /////////////////////////////////////////////// /////////////////////////////////////////////// /////////////////////////////////////////////// -TEST(image_hfilter, pixel_hfilter__acc_init) +TEST(pixel_hfilter, pixel_hfilter__acc_init) { static const int8_t coef_count = 32; static const unsigned output_count = 16; @@ -95,7 +97,7 @@ TEST(image_hfilter, pixel_hfilter__acc_init) /////////////////////////////////////////////// /////////////////////////////////////////////// /////////////////////////////////////////////// -TEST(image_hfilter, pixel_hfilter__apply_shift) +TEST(pixel_hfilter, pixel_hfilter__apply_shift) { static const int8_t coef_count = 32; static const unsigned output_count = 16; @@ -125,7 +127,7 @@ TEST(image_hfilter, pixel_hfilter__apply_shift) /////////////////////////////////////////////// /////////////////////////////////////////////// /////////////////////////////////////////////// -TEST(image_hfilter, pixel_hfilter__input_stride) +TEST(pixel_hfilter, pixel_hfilter__input_stride) { static const int8_t coef_count = 32; static const unsigned output_count = 16; @@ -154,7 +156,7 @@ TEST(image_hfilter, pixel_hfilter__input_stride) /////////////////////////////////////////////// /////////////////////////////////////////////// /////////////////////////////////////////////// -TEST(image_hfilter, pixel_hfilter__out_count) +TEST(pixel_hfilter, pixel_hfilter__out_count) { static const int8_t coef_count = 32; static const unsigned output_count = 64; @@ -183,7 +185,7 @@ TEST(image_hfilter, pixel_hfilter__out_count) /////////////////////////////////////////////// /////////////////////////////////////////////// /////////////////////////////////////////////// -TEST(image_hfilter, pixel_hfilter__alt_coef) +TEST(pixel_hfilter, pixel_hfilter__alt_coef) { static const int8_t coef_count = 32; static const unsigned output_count = 32; @@ -224,7 +226,37 @@ TEST(image_hfilter, pixel_hfilter__alt_coef) /////////////////////////////////////////////// /////////////////////////////////////////////// /////////////////////////////////////////////// -TEST(image_hfilter, image_hfilter_update_scale__case1) +TEST(pixel_hfilter, pixel_hfilter__timing) +{ + int8_t coef[32] = {0}; + int8_t input[32] = {0}; + int8_t output[64] = {0}; + + unsigned t_16x[4]; + + for(int k = 0; k < 4; k++){ + unsigned ts = measure_time(); + pixel_hfilter(output, input, coef, 0, 0 , 0, 16*(k+1)); + unsigned te = measure_time(); + t_16x[k] = te - ts; + } + + printf("\n"); + printf("\tout_count: 16 32 48 64\n"); + printf("\tticks: %8u %8u %8u %8u\n", + t_16x[0], t_16x[1], t_16x[2], t_16x[3]); + +} + + + + + + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(pixel_hfilter, pixel_hfilter_update_scale__case1) { hfilter_state_t state; @@ -232,7 +264,7 @@ TEST(image_hfilter, image_hfilter_update_scale__case1) const float gain = 0.0f; - image_hfilter_update_scale(&state, gain, 0); + pixel_hfilter_update_scale(&state, gain, 0); const unsigned exp_shift = 9; const int32_t exp_acc_init = -128 * (1< Date: Fri, 9 Jun 2023 08:50:58 -0400 Subject: [PATCH 061/306] Changes default channel gains to 1.0 --- camera/src/packet_handler.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index 2bb54f30..588de78c 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -35,12 +35,12 @@ static struct { hfilter_state_t hfilter_state[APP_IMAGE_CHANNEL_COUNT]; // Initial channel scales -#define AWB_gain_RED 1.3 -#define AWB_gain_GREEN 1.3 -#define AWB_gain_BLUE 0.8 -// #define AWB_gain_RED 1.0 -// #define AWB_gain_GREEN 1.0 -// #define AWB_gain_BLUE 1.0 +// #define AWB_gain_RED 1.3 +// #define AWB_gain_GREEN 1.3 +// #define AWB_gain_BLUE 0.8 +#define AWB_gain_RED 1.0 +#define AWB_gain_GREEN 1.0 +#define AWB_gain_BLUE 1.0 isp_params_t isp_params = { .channel_gain = { From e6ea7897d15df64b11bcd457f23ed13ed112fe41 Mon Sep 17 00:00:00 2001 From: Aaron Stewart Date: Fri, 9 Jun 2023 08:51:53 -0400 Subject: [PATCH 062/306] Fixes broken #define missed in previous commit --- modules/mipi/api/mipi_defines.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/mipi/api/mipi_defines.h b/modules/mipi/api/mipi_defines.h index 810d6701..a2a4c1ba 100644 --- a/modules/mipi/api/mipi_defines.h +++ b/modules/mipi/api/mipi_defines.h @@ -171,7 +171,7 @@ typedef enum xMIPI_DemuxMode_t { #define _LANE_SWAP_LAN0_DEFAULT (0 << 3) #define _LANE_SWAP_CLK_DEFAULT (2 << 0) -#define DEFAULT_MIPI_SHIM_CFG3 ( \ +#define DEFAULT_MIPI_DPHY_CFG3 ( \ _ENABLE_LAN1 |\ _ENABLE_LAN0 |\ _ENABLE_CLK |\ From 51c781cc39889ef73dc66d1b1939cdc33eb38145 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Fri, 9 Jun 2023 14:21:52 +0100 Subject: [PATCH 063/306] adding bmp xscope api --- examples/take_picture_downsample/src/app.c | 2 +- examples/take_picture_downsample/src/main.xc | 6 ++ utils/io_utils/io_utils.c | 86 +++++++++++++++++++- utils/io_utils/io_utils.h | 2 + 4 files changed, 94 insertions(+), 2 deletions(-) diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index 42daa6a6..65bbe92e 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -30,7 +30,7 @@ void user_app(streaming_chanend_t c_cam_api){ write_image_file("capture.bin", io_buff, APP_IMAGE_HEIGHT_PIXELS, APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_CHANNEL_COUNT); //save it to bmp - writeBMP("capture.bmp", temp_buffer); + write_bmp_file("capture.bmp", io_buff, APP_IMAGE_HEIGHT_PIXELS, APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_CHANNEL_COUNT); // end here exit(0); diff --git a/examples/take_picture_downsample/src/main.xc b/examples/take_picture_downsample/src/main.xc index be13aa72..0e1269f6 100644 --- a/examples/take_picture_downsample/src/main.xc +++ b/examples/take_picture_downsample/src/main.xc @@ -12,6 +12,9 @@ on tile[0]: port p_scl = XS1_PORT_1N; on tile[0]: port p_sda = XS1_PORT_1O; +extern "C" { +#include "xscope_io_device.h" +} /** * Declaration of the MIPI interface ports: @@ -28,7 +31,9 @@ int main(void) { streaming chan c_cam_api; i2c_master_if i2c[1]; + chan xscope_chan; par { + xscope_host_data(xscope_chan); on tile[0]: i2c_master(i2c, 1, p_scl, p_sda, Kbps); on tile[MIPI_TILE]: camera_main(tile[MIPI_TILE], @@ -40,6 +45,7 @@ int main(void) i2c[0], c_cam_api); + on tile[MIPI_TILE]: xscope_io_init(xscope_chan); on tile[MIPI_TILE]: user_app(c_cam_api); } return 0; diff --git a/utils/io_utils/io_utils.c b/utils/io_utils/io_utils.c index c0935bf2..b810e6f2 100644 --- a/utils/io_utils/io_utils.c +++ b/utils/io_utils/io_utils.c @@ -4,7 +4,7 @@ void swap_dimentions(uint8_t * image_in, uint8_t * image_out, const size_t height, const size_t width, const size_t channels) { - printf("Swapping image dimentions\n"); + printf("Swapping image dimentions...\n"); for(size_t k = 0; k < height; k++) { for(size_t j = 0; j < width; j++) @@ -38,3 +38,87 @@ void write_image_file(char * filename, uint8_t * image, const size_t height, con printf("Image written into file: %s\n", filename); printf("Image dimentions: %d x %d\n", width, height); } + + +void write_bmp_file(char * filename, uint8_t * image, const size_t height, const size_t width, const size_t channels) +{ + const size_t file_header_len = 14; + const size_t info_header_len = 40; + + // Define BMP file header and info header + unsigned char bmpFileHeader[file_header_len] = { + 'B', 'M', // Signature + 0, 0, 0, 0, // File size (to be filled later) + 0, 0, // Reserved + 0, 0, // Reserved + 54, 0, 0, 0 // Offset to image data + }; + + unsigned char bmpInfoHeader[info_header_len] = { + 40, 0, 0, 0, // Info header size + 0, 0, 0, 0, // Image width (to be filled later) + 0, 0, 0, 0, // Image height (to be filled later) + 1, 0, // Number of color planes + 8 * channels, 0, // Bits per pixel + 0, 0, 0, 0, // Compression method + 0, 0, 0, 0, // Image size (to be filled later) + 0, 0, 0, 0, // Horizontal resolution (pixel per meter) + 0, 0, 0, 0, // Vertical resolution (pixel per meter) + 0, 0, 0, 0, // Number of colors in the palette + 0, 0, 0, 0, // Number of important colors + }; + + // Calculate the row size (including padding) + size_t rowSize = width * channels; + size_t paddingSize = (4 - (rowSize % 4)) % 4; + size_t rowSizeWithPadding = rowSize + paddingSize; + + // Calculate the file size + size_t fileSize = 54 + (rowSizeWithPadding * height); + + // Update the file size in the BMP file header + bmpFileHeader[2] = (unsigned char)(fileSize); + bmpFileHeader[3] = (unsigned char)(fileSize >> 8); + bmpFileHeader[4] = (unsigned char)(fileSize >> 16); + bmpFileHeader[5] = (unsigned char)(fileSize >> 24); + + // Update the image width in the BMP info header + bmpInfoHeader[4] = (unsigned char)(width); + bmpInfoHeader[5] = (unsigned char)(width >> 8); + bmpInfoHeader[6] = (unsigned char)(width >> 16); + bmpInfoHeader[7] = (unsigned char)(width >> 24); + + // Update the image height in the BMP info header + bmpInfoHeader[8] = (unsigned char)(height); + bmpInfoHeader[9] = (unsigned char)(height >> 8); + bmpInfoHeader[10] = (unsigned char)(height >> 16); + bmpInfoHeader[11] = (unsigned char)(height >> 24); + + printf("Writing bmp image...\n"); + + xscope_file_t fp = xscope_open_file(filename, "wb"); + + xscope_fwrite(&fp, bmpFileHeader, file_header_len * sizeof(unsigned char)); + xscope_fwrite(&fp, bmpInfoHeader, info_header_len * sizeof(unsigned char)); + + unsigned char padding_array[paddingSize]; + memset(padding_array, (int)'\0', paddingSize); + for(size_t i = height - 1; i >= 0; i--) + { + for(size_t j = 0; j < width; j++) + { + // Write the pixel data (assuming RGB order) + xscope_fwrite(&fp, &img[i][j][2], 1 * sizeof(unsigned char)); // Blue + xscope_fwrite(&fp, &img[i][j][1], 1 * sizeof(unsigned char)); // Green + xscope_fwrite(&fp, &img[i][j][0], 1 * sizeof(unsigned char)); // Red + // For 4-channel images, you can write the alpha channel here + // not sure about the comemnt below + //xscope_fwrite(&fp, &img[i][j][3], 1 * sizeof(unsigned char)); // Alpha + } + xscope_fwrite(&fp, padding_array, paddingSize * sizeof(unsigned char)); + } + + xscope_close_all_files(); + printf("Image written into file: %s\n", filename); + printf("Image dimentions: %d x %d\n", width, height); +} \ No newline at end of file diff --git a/utils/io_utils/io_utils.h b/utils/io_utils/io_utils.h index fe365d30..4c77a86b 100644 --- a/utils/io_utils/io_utils.h +++ b/utils/io_utils/io_utils.h @@ -14,6 +14,8 @@ void swap_dimentions(uint8_t * image_in, uint8_t * image_out, const size_t heigh void write_image_file(char * filename, uint8_t * image, const size_t height, const size_t width, const size_t channels); +void write_bmp_file(char * filename, uint8_t * image, const size_t height, const size_t width, const size_t channels); + #ifdef __XC__ } #endif From 2f75d1fcb40bfe0472560b4bf55acd9f4132684e Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Fri, 9 Jun 2023 14:38:36 +0100 Subject: [PATCH 064/306] fixing array adressing --- utils/io_utils/io_utils.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/utils/io_utils/io_utils.c b/utils/io_utils/io_utils.c index b810e6f2..c803e6a5 100644 --- a/utils/io_utils/io_utils.c +++ b/utils/io_utils/io_utils.c @@ -103,17 +103,18 @@ void write_bmp_file(char * filename, uint8_t * image, const size_t height, const unsigned char padding_array[paddingSize]; memset(padding_array, (int)'\0', paddingSize); - for(size_t i = height - 1; i >= 0; i--) + for(int64_t i = height - 1; i >= 0; i--) { for(size_t j = 0; j < width; j++) { // Write the pixel data (assuming RGB order) - xscope_fwrite(&fp, &img[i][j][2], 1 * sizeof(unsigned char)); // Blue - xscope_fwrite(&fp, &img[i][j][1], 1 * sizeof(unsigned char)); // Green - xscope_fwrite(&fp, &img[i][j][0], 1 * sizeof(unsigned char)); // Red + size_t offset = i * (channels * width) + j * channels - 1; + xscope_fwrite(&fp, &image[offset + 2], 1 * sizeof(unsigned char)); // Blue + xscope_fwrite(&fp, &image[offset + 1], 1 * sizeof(unsigned char)); // Green + xscope_fwrite(&fp, &image[offset + 0], 1 * sizeof(unsigned char)); // Red // For 4-channel images, you can write the alpha channel here // not sure about the comemnt below - //xscope_fwrite(&fp, &img[i][j][3], 1 * sizeof(unsigned char)); // Alpha + //xscope_fwrite(&fp, &image[offset + 3], 1 * sizeof(unsigned char)); // Alpha } xscope_fwrite(&fp, padding_array, paddingSize * sizeof(unsigned char)); } @@ -121,4 +122,4 @@ void write_bmp_file(char * filename, uint8_t * image, const size_t height, const xscope_close_all_files(); printf("Image written into file: %s\n", filename); printf("Image dimentions: %d x %d\n", width, height); -} \ No newline at end of file +} From baaae3301c85cb5928d61c1277bda8dfaacac4fa Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Fri, 9 Jun 2023 14:54:28 +0100 Subject: [PATCH 065/306] fixing zero lenght array issue --- utils/io_utils/io_utils.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/utils/io_utils/io_utils.c b/utils/io_utils/io_utils.c index c803e6a5..84ca277c 100644 --- a/utils/io_utils/io_utils.c +++ b/utils/io_utils/io_utils.c @@ -101,14 +101,12 @@ void write_bmp_file(char * filename, uint8_t * image, const size_t height, const xscope_fwrite(&fp, bmpFileHeader, file_header_len * sizeof(unsigned char)); xscope_fwrite(&fp, bmpInfoHeader, info_header_len * sizeof(unsigned char)); - unsigned char padding_array[paddingSize]; - memset(padding_array, (int)'\0', paddingSize); for(int64_t i = height - 1; i >= 0; i--) { for(size_t j = 0; j < width; j++) { // Write the pixel data (assuming RGB order) - size_t offset = i * (channels * width) + j * channels - 1; + size_t offset = i * (channels * width) + j * channels - 1; xscope_fwrite(&fp, &image[offset + 2], 1 * sizeof(unsigned char)); // Blue xscope_fwrite(&fp, &image[offset + 1], 1 * sizeof(unsigned char)); // Green xscope_fwrite(&fp, &image[offset + 0], 1 * sizeof(unsigned char)); // Red @@ -116,7 +114,12 @@ void write_bmp_file(char * filename, uint8_t * image, const size_t height, const // not sure about the comemnt below //xscope_fwrite(&fp, &image[offset + 3], 1 * sizeof(unsigned char)); // Alpha } - xscope_fwrite(&fp, padding_array, paddingSize * sizeof(unsigned char)); + if(paddingSize) + { + unsigned char padding_array[paddingSize]; + memset(padding_array, (int)'\0', paddingSize); + xscope_fwrite(&fp, padding_array, paddingSize * sizeof(unsigned char)); + } } xscope_close_all_files(); From fd7c5ebfbbee3243169c9ed22ea9fdcac5b5303f Mon Sep 17 00:00:00 2001 From: Aaron Stewart Date: Fri, 9 Jun 2023 11:42:09 -0400 Subject: [PATCH 066/306] Fixes issue with MIPI shim configuration. --- camera/api/camera.h | 30 ++++++++++++++++----- camera/src/camera.xc | 22 +++++++-------- camera/src/packet_handler.c | 12 ++++----- modules/mipi/api/mipi_defines.h | 10 +++---- modules/mipi/src/MipiPacketRx.S | 2 -- python/decode_downsampled.py | 2 +- python/decode_raw8.py | 5 ++-- sensors/api/sensor.h | 1 - tests/hardware_tests/test_timing/src/app.xc | 16 ++++++----- tests/unit_tests/CMakeLists.txt | 2 +- 10 files changed, 58 insertions(+), 44 deletions(-) diff --git a/camera/api/camera.h b/camera/api/camera.h index bb9added..3debabcc 100644 --- a/camera/api/camera.h +++ b/camera/api/camera.h @@ -25,13 +25,29 @@ #include "isp.h" -/** - * The packet buffer is where the packet decoupler will tell the MIPI receiver - * thread to store received packets. - */ -#define DEMUX_DATATYPE 0 // RESERVED -#define DEMUX_MODE CONFIG_DEMUX_MODE // xor bias or not -#define DEMUX_EN 0 // DISABLE DEMUX +/// Indicates whether the demux is enabled on the MIPI shim +/// 0 = disabled, 1 = enabled +#define MIPI_SHIM_DEMUX_EN 0 +/// The MIPI datatype that is to be demuxed +/// Unused if MIPI_SHIM_DEMUX_EN = 0 +#define MIPI_SHIM_DEMUX_DATATYPE 0 +/// The MIPI shim demux mode (see xMIPI_DemuxMode_t) +/// Unused if MIPI_SHIM_DEMUX_EN = 0 +#define MIPI_SHIM_DEMUX_MODE 0 +/// Indicates whether the MIPI shim should apply a bias to received pixel data. +/// 0 = disabled, 1 = enabled (does not require MIPI_SHIM_DEMUX_EN = 1) +/// The bias subtracts 128 from each received pixel value, converting the +/// pixel data from uint8 to int8. +/// TODO: astew: This is currently ignored -- the bias is always enabled in the +/// downsample app, and disabled in the raw capture app. +#define MIPI_SHIM_BIAS_ENABLE 1 +/// Indicates whether the MIPI shim should stuff an extra byte into each triplet +/// of received pixel data, in order to achieve 4-byte alignment of pixel +/// values. +/// 0 = disabled, 1 = enabled (does not require MIPI_SHIM_DEMUX_EN = 1) +#define MIPI_SHIM_STUFF_ENABLE 0 // Enable byte stuffing + + #define MIPI_CLK_DIV 1 // CLK DIVIDER #define MIPI_CFG_CLK_DIV 3 // CFG DIVIDER diff --git a/camera/src/camera.xc b/camera/src/camera.xc index ec3367f5..f93e2104 100644 --- a/camera/src/camera.xc +++ b/camera/src/camera.xc @@ -42,13 +42,13 @@ void camera_main( XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, DEFAULT_MIPI_DPHY_CFG3); - unsigned mipi_shim_cfg0 = MIPI_SHIM_CFG0_PACK(DEMUX_EN, - DEMUX_DATATYPE, - DEMUX_MODE, - 0, - (DEMUX_MODE)); + // Configure MIPI shim + unsigned mipi_shim_cfg0 = MIPI_SHIM_CFG0_PACK(MIPI_SHIM_DEMUX_EN, + MIPI_SHIM_DEMUX_DATATYPE, + MIPI_SHIM_DEMUX_MODE, + MIPI_SHIM_STUFF_ENABLE, + 1); // enable bias - // send packet to MIPI shim MipiPacketRx_init(mipi_tile, p_mipi_rxd, p_mipi_rxv, @@ -105,11 +105,11 @@ void camera_main_raw( XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, DEFAULT_MIPI_DPHY_CFG3); //TODO decompose into different values - unsigned mipi_shim_cfg0 = MIPI_SHIM_CFG0_PACK(DEMUX_EN, - DEMUX_DATATYPE, - DEMUX_MODE, - 0, - (DEMUX_MODE)); + unsigned mipi_shim_cfg0 = MIPI_SHIM_CFG0_PACK(MIPI_SHIM_DEMUX_EN, + MIPI_SHIM_DEMUX_DATATYPE, + MIPI_SHIM_DEMUX_MODE, + MIPI_SHIM_STUFF_ENABLE, + 0); // disable bias // send packet to MIPI shim MipiPacketRx_init(mipi_tile, diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index 588de78c..b171866c 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -35,12 +35,12 @@ static struct { hfilter_state_t hfilter_state[APP_IMAGE_CHANNEL_COUNT]; // Initial channel scales -// #define AWB_gain_RED 1.3 -// #define AWB_gain_GREEN 1.3 -// #define AWB_gain_BLUE 0.8 -#define AWB_gain_RED 1.0 -#define AWB_gain_GREEN 1.0 -#define AWB_gain_BLUE 1.0 +#define AWB_gain_RED 1.3 +#define AWB_gain_GREEN 0.8 +#define AWB_gain_BLUE 1.3 +// #define AWB_gain_RED 1.0 +// #define AWB_gain_GREEN 1.0 +// #define AWB_gain_BLUE 1.0 isp_params_t isp_params = { .channel_gain = { diff --git a/modules/mipi/api/mipi_defines.h b/modules/mipi/api/mipi_defines.h index a2a4c1ba..1f6fd3d5 100644 --- a/modules/mipi/api/mipi_defines.h +++ b/modules/mipi/api/mipi_defines.h @@ -151,11 +151,11 @@ typedef enum xMIPI_DemuxMode_t { 0 _PIXEL_DEMUX_EN Enable pixel demuxing */ #define MIPI_SHIM_CFG0_PACK(DMUX_EN, DMUX_DT, DMUX_MODE, DMUX_STFF, DMUX_BIAS) \ - ( (((DMUX_EN) & 0x1 ) << 0) \ - | (((DMUX_DT) & 0xFF ) << 8) \ - | (((DMUX_MODE) & 0x3F ) << 16) \ - | (((DMUX_STFF) & 0x1 ) << 22) \ - | (((DMUX_BIAS) & 0x1 ) << 23)) + ( (((DMUX_EN != 0) & 0x1 ) << 0) \ + | (((DMUX_DT) & 0xFF ) << 8) \ + | (((DMUX_MODE) & 0x3F ) << 16) \ + | (((DMUX_STFF != 0) & 0x1 ) << 22) \ + | (((DMUX_BIAS != 0) & 0x1 ) << 23)) diff --git a/modules/mipi/src/MipiPacketRx.S b/modules/mipi/src/MipiPacketRx.S index a2c5617a..3cd439fd 100644 --- a/modules/mipi/src/MipiPacketRx.S +++ b/modules/mipi/src/MipiPacketRx.S @@ -190,8 +190,6 @@ or r8, r8, r9 { in S0, res[P_RXD] ; add BUFF, BUFF, 4 } .L_RX_LONG_LOOP: - // { byterev S0, S0 ; } - // { xor S0, S0, r8 ; } { add BUFF, BUFF, 4 ; stw S0, BUFF[0] } { in S0, res[P_RXD] ; bu .L_RX_LONG_LOOP } diff --git a/python/decode_downsampled.py b/python/decode_downsampled.py index 5fd8d144..93463edf 100644 --- a/python/decode_downsampled.py +++ b/python/decode_downsampled.py @@ -13,7 +13,7 @@ from utils import * -input_name = os.getenv('BINARY_IMG_PATH') +input_name = os.getenv('BINARY_IMG_PATH') or "capture.bin" # read the data with open(input_name, "rb") as f: diff --git a/python/decode_raw8.py b/python/decode_raw8.py index 224e91ca..61a0e98d 100644 --- a/python/decode_raw8.py +++ b/python/decode_raw8.py @@ -21,7 +21,7 @@ from utils import * -input_name = os.getenv('BINARY_IMG_PATH') +input_name = os.getenv('BINARY_IMG_PATH') or "capture.bin" #input_name = Path(__file__).parent / "capture.bin" width, height = 640, 480 @@ -48,8 +48,7 @@ # unpack -buffer = np.frombuffer(data, dtype=np.int8) + 128 # convert to uint8 -buffer = buffer.astype(np.uint8) +buffer = np.frombuffer(data, dtype=np.uint8) # data is uint8 img = buffer.reshape(height, width, 1) print("unpacked_data") diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index 3db9ee08..685cfa61 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -18,7 +18,6 @@ // Mipi format and mode #define CONFIG_MIPI_FORMAT MIPI_DT_RAW8 -#define CONFIG_DEMUX_MODE BIAS_ENABLED #define MIPI_PKT_BUFFER_COUNT 4 // FPS settings diff --git a/tests/hardware_tests/test_timing/src/app.xc b/tests/hardware_tests/test_timing/src/app.xc index 8515b16e..c7e8c38c 100644 --- a/tests/hardware_tests/test_timing/src/app.xc +++ b/tests/hardware_tests/test_timing/src/app.xc @@ -495,10 +495,6 @@ void printPacketLogSummary( static packet_timing_t packet_log[TABLE_ROWS]; -#define DEMUX_DATATYPE 0 -#define DEMUX_MODE 0x00 // no demux -#define DEMUX_EN 0 - #define MIPI_CLK_DIV 1 #define MIPI_CFG_CLK_DIV 2 @@ -509,11 +505,17 @@ void mipi_main( write_node_config_reg(tile[MIPI_TILE], XS1_SSWITCH_MIPI_DPHY_CFG3_NUM , 0x7E42); + unsigned mipi_shim_cfg0 = MIPI_SHIM_CFG0_PACK(0,0,0,0,0); MipiPacketRx_init(tile[MIPI_TILE], - p_mipi_rxd, p_mipi_rxv, p_mipi_rxa, p_mipi_clk, clk_mipi, - DEMUX_EN, DEMUX_DATATYPE, DEMUX_MODE, - MIPI_CLK_DIV, MIPI_CFG_CLK_DIV); + p_mipi_rxd, + p_mipi_rxv, + p_mipi_rxa, + p_mipi_clk, + clk_mipi, + mipi_shim_cfg0, + MIPI_CLK_DIV, + MIPI_CFG_CLK_DIV); // Start camera and its configurations int r = 0; diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index cc76595d..d206528e 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -38,7 +38,7 @@ set(APP_COMPILE_DEFINITIONS # ############################################################################## # Create executable # ############################################################################## -add_executable(${TARGET} EXCLUDE_FROM_ALL) +add_executable(${TARGET} ) target_sources(${TARGET} PUBLIC ${APP_SOURCES}) target_include_directories(${TARGET} PUBLIC ${APP_INCLUDES}) target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) From d096605e1e3006ccde9204c7b31d3b56cd3108b1 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Fri, 9 Jun 2023 16:45:18 +0100 Subject: [PATCH 067/306] clearer bmp header --- utils/io_utils/io_utils.c | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/utils/io_utils/io_utils.c b/utils/io_utils/io_utils.c index 84ca277c..43234554 100644 --- a/utils/io_utils/io_utils.c +++ b/utils/io_utils/io_utils.c @@ -39,11 +39,11 @@ void write_image_file(char * filename, uint8_t * image, const size_t height, con printf("Image dimentions: %d x %d\n", width, height); } - void write_bmp_file(char * filename, uint8_t * image, const size_t height, const size_t width, const size_t channels) { const size_t file_header_len = 14; const size_t info_header_len = 40; + const size_t img_offset = file_header_len + info_header_len; // Define BMP file header and info header unsigned char bmpFileHeader[file_header_len] = { @@ -51,11 +51,11 @@ void write_bmp_file(char * filename, uint8_t * image, const size_t height, const 0, 0, 0, 0, // File size (to be filled later) 0, 0, // Reserved 0, 0, // Reserved - 54, 0, 0, 0 // Offset to image data + img_offset, 0, 0, 0 // Offset to image data }; unsigned char bmpInfoHeader[info_header_len] = { - 40, 0, 0, 0, // Info header size + info_header_len, 0, 0, 0, // Info header size 0, 0, 0, 0, // Image width (to be filled later) 0, 0, 0, 0, // Image height (to be filled later) 1, 0, // Number of color planes @@ -74,23 +74,23 @@ void write_bmp_file(char * filename, uint8_t * image, const size_t height, const size_t rowSizeWithPadding = rowSize + paddingSize; // Calculate the file size - size_t fileSize = 54 + (rowSizeWithPadding * height); + size_t fileSize = img_offset + (rowSizeWithPadding * height); // Update the file size in the BMP file header bmpFileHeader[2] = (unsigned char)(fileSize); - bmpFileHeader[3] = (unsigned char)(fileSize >> 8); + bmpFileHeader[3] = (unsigned char)(fileSize >> 8); bmpFileHeader[4] = (unsigned char)(fileSize >> 16); bmpFileHeader[5] = (unsigned char)(fileSize >> 24); // Update the image width in the BMP info header bmpInfoHeader[4] = (unsigned char)(width); - bmpInfoHeader[5] = (unsigned char)(width >> 8); + bmpInfoHeader[5] = (unsigned char)(width >> 8); bmpInfoHeader[6] = (unsigned char)(width >> 16); bmpInfoHeader[7] = (unsigned char)(width >> 24); // Update the image height in the BMP info header - bmpInfoHeader[8] = (unsigned char)(height); - bmpInfoHeader[9] = (unsigned char)(height >> 8); + bmpInfoHeader[8] = (unsigned char)(height); + bmpInfoHeader[9] = (unsigned char)(height >> 8); bmpInfoHeader[10] = (unsigned char)(height >> 16); bmpInfoHeader[11] = (unsigned char)(height >> 24); @@ -107,12 +107,12 @@ void write_bmp_file(char * filename, uint8_t * image, const size_t height, const { // Write the pixel data (assuming RGB order) size_t offset = i * (channels * width) + j * channels - 1; - xscope_fwrite(&fp, &image[offset + 2], 1 * sizeof(unsigned char)); // Blue - xscope_fwrite(&fp, &image[offset + 1], 1 * sizeof(unsigned char)); // Green - xscope_fwrite(&fp, &image[offset + 0], 1 * sizeof(unsigned char)); // Red + xscope_fwrite(&fp, &image[offset + 2], 1 * sizeof(uint8_t)); // Blue + xscope_fwrite(&fp, &image[offset + 1], 1 * sizeof(uint8_t)); // Green + xscope_fwrite(&fp, &image[offset + 0], 1 * sizeof(uint8_t)); // Red // For 4-channel images, you can write the alpha channel here // not sure about the comemnt below - //xscope_fwrite(&fp, &image[offset + 3], 1 * sizeof(unsigned char)); // Alpha + //xscope_fwrite(&fp, &image[offset + 3], 1 * sizeof(uint8_t)); // Alpha } if(paddingSize) { From 3b43dc410a9e74be73038046309cbe7eeec16027 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Fri, 9 Jun 2023 17:46:39 +0100 Subject: [PATCH 068/306] minor fixes --- camera/api/isp.h | 15 +++++++++++ camera/api/utils.h | 2 ++ camera/src/utils.c | 15 +++++++++++ examples/take_picture_downsample/src/app.c | 2 +- examples/take_picture_raw/src/app_raw.c | 3 ++- python/decode_downsampled.py | 4 +-- python/decode_raw10.py | 13 +++++++-- python/decode_raw8.py | 31 +++++++++++++++------- sensors/imx219.xc | 13 +++------ 9 files changed, 72 insertions(+), 26 deletions(-) diff --git a/camera/api/isp.h b/camera/api/isp.h index 6b608022..9199731f 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -44,4 +44,19 @@ void isp_bilinear_resize( void isp_rotate_image(const uint8_t *src, uint8_t *dest, int width, int height); +// -------------------------- COLOR CONVERSION ------------------------------------- +// Macro arguments +#define MASK8 0xFF +#define MASK24 0x00FFFFFF +#define GET_R(x) ((int8_t)((x & MASK24)) & MASK8) +#define GET_G(x) ((int8_t)((x & MASK24) >> 8) & MASK8) +#define GET_B(x) ((int8_t)((x & MASK24) >> 16) & MASK8) +#define GET_Y(x) GET_R(x) +#define GET_U(x) GET_G(x) +#define GET_V(x) GET_B(x) + +// Converts signed yuv or rgb (-127..127, -127..127, -127..127) into signed rgb / yuv. +extern int yuv_to_rgb(int y, int u, int v); +extern int rgb_to_yuv(int r, int g, int b); + #endif // ISP_H diff --git a/camera/api/utils.h b/camera/api/utils.h index a34f323a..98c9ae37 100644 --- a/camera/api/utils.h +++ b/camera/api/utils.h @@ -30,6 +30,8 @@ void img_int8_to_uint8( uint8_t out_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS] ); +void img_int8_to_uint8_inplace(const size_t width, const size_t height, int8_t *image_buffer); + #if defined(__XC__) || defined(__cplusplus) } #endif diff --git a/camera/src/utils.c b/camera/src/utils.c index 7e6a8013..e1e0c4b5 100644 --- a/camera/src/utils.c +++ b/camera/src/utils.c @@ -179,3 +179,18 @@ void img_int8_to_uint8( } } } + + +// Convert int8_t to uint8_t image +void img_int8_to_uint8_inplace( + const size_t width, + const size_t height, + int8_t* image_buffer +) +{ + // Add 128 to all elements + for (size_t i = 0; i < width * height; i++) + { + image_buffer[i] = (uint8_t)(image_buffer[i] + 128); + } +} diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index 54476669..27f6b918 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -15,7 +15,7 @@ void user_app(streaming_chanend_t c_cam_api){ delay_milliseconds(5000); printf("Requesting image...\n"); camera_capture_image(image_buffer, c_cam_api); - printf("Image captured...\n"); + printf("Image captured.\n"); // Rotate 180 degrees // rotate_image(image_buffer); diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index c20bf9dc..19d8a1f6 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -22,9 +22,10 @@ void user_app_raw(streaming_chanend_t c_cam_api){ // Request an image printf("Requesting image...\n"); camera_capture_image_raw(image_buffer, c_cam_api); - printf("Image captured...\n"); + printf("Image captured.\n"); // Save the image to a file + img_int8_to_uint8_inplace(W_RAW, H_RAW, image_buffer); write_image_file("capture.bin", (uint8_t * ) &image_buffer[0], MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES, 1); exit(0); } diff --git a/python/decode_downsampled.py b/python/decode_downsampled.py index 5fd8d144..1238ab90 100644 --- a/python/decode_downsampled.py +++ b/python/decode_downsampled.py @@ -3,7 +3,7 @@ * 160x120 * R G B """ - +import os import cv2 import matplotlib.pyplot as plt import numpy as np @@ -11,7 +11,7 @@ from dotenv import load_dotenv load_dotenv() # take environment variables from .env. -from utils import * +from utils import show_histogram_by_channel input_name = os.getenv('BINARY_IMG_PATH') diff --git a/python/decode_raw10.py b/python/decode_raw10.py index 9b7d6084..5c7257a9 100644 --- a/python/decode_raw10.py +++ b/python/decode_raw10.py @@ -6,6 +6,7 @@ BGGR is the order of the Bayer pattern few padding bytes on the end of every row to match bits """ +import os import cv2 import matplotlib.pyplot as plt import numpy as np @@ -13,7 +14,15 @@ from dotenv import load_dotenv load_dotenv() # take environment variables from .env. -from utils import * +from utils import ( + remove_padding_buffer_no_align, + unpack_mipi_raw10_buffer, + old_normalize, + white_balance, + demosaic, + new_color_correction, + plot_imgs +) input_name = os.getenv('BINARY_IMG_PATH') @@ -89,4 +98,4 @@ blue = int(img_demoisaic[:,:,2].sum()) txt = f"blue: {blue}, green: {green}, red: {red}" -print(txt) \ No newline at end of file +print(txt) diff --git a/python/decode_raw8.py b/python/decode_raw8.py index 224e91ca..c21a77c9 100644 --- a/python/decode_raw8.py +++ b/python/decode_raw8.py @@ -11,7 +11,7 @@ BGGR is the order of the Bayer pattern few padding bytes on the end of every row to match bits """ - +import os import cv2 import matplotlib.pyplot as plt import numpy as np @@ -19,10 +19,22 @@ from dotenv import load_dotenv load_dotenv() # take environment variables from .env. -from utils import * +from utils import ( + normalize, + simple_white_balance, + demosaic, + new_color_correction, + plot_imgs +) + +from pathlib import Path +import os +import cv2 +import numpy as np +from PIL import Image -input_name = os.getenv('BINARY_IMG_PATH') -#input_name = Path(__file__).parent / "capture.bin" +#input_name = os.getenv('BINARY_IMG_PATH') +input_name = Path(__file__).parent / "capture.bin" width, height = 640, 480 @@ -48,8 +60,9 @@ # unpack -buffer = np.frombuffer(data, dtype=np.int8) + 128 # convert to uint8 -buffer = buffer.astype(np.uint8) +#buffer = np.frombuffer(data, dtype=np.int8) + 128 # convert to uint8 +#buffer = buffer.astype(np.uint8) +buffer = np.frombuffer(data, dtype=np.uint8) img = buffer.reshape(height, width, 1) print("unpacked_data") @@ -83,9 +96,6 @@ img = cv2.resize(img, (width // kfactor, height // kfactor), interpolation=cv2.INTER_AREA) # ------ The ISP pipeline ------------------------- - - -###################################################### ################# PLOT ############################## # save image @@ -93,6 +103,7 @@ imgs = cv2.flip(img, 0) else: imgs = img + name = f"{input_name}_postprocess_.png" Image.fromarray(imgs).save(name) print(name) @@ -107,4 +118,4 @@ blue = int(img_demoisaic[:,:,2].sum()) txt = f"blue: {blue}, green: {green}, red: {red}" -print(txt) \ No newline at end of file +print(txt) diff --git a/sensors/imx219.xc b/sensors/imx219.xc index 333a61c4..44f957bb 100644 --- a/sensors/imx219.xc +++ b/sensors/imx219.xc @@ -50,7 +50,6 @@ static int i2c_write_table(client interface i2c_master_if i2c, } - static int i2c_write_table_val(client interface i2c_master_if i2c, imx219_settings_t table[], int N) { @@ -72,8 +71,8 @@ static int i2c_write_table_val(client interface i2c_master_if i2c, address &= 0x7fff; } mode = 'c'; - printf("mode=%c , address = 0x%04x, value = 0x%02x\n", mode, address, value >> 8); - printf("mode=%c , address+ = 0x%04x, value = 0x%02x\n", mode, address+1, value & 0xff); + //printf("mode=%c , address = 0x%04x, value = 0x%02x\n", mode, address, value >> 8); + //printf("mode=%c , address+ = 0x%04x, value = 0x%02x\n", mode, address+1, value & 0xff); // continous writte ret = i2c_write(i2c, address, value >> 8); // B1 B2 B3 B4 -> B1 B2 ret |= i2c_write(i2c, address+1, value & 0xff); // B1 B2 B3 B4 -> B3 B4 @@ -83,7 +82,7 @@ static int i2c_write_table_val(client interface i2c_master_if i2c, // single writte mode = 's'; ret = i2c_write(i2c, address, value); - printf("mode=%c , address = 0x%04x, value = 0x%02x\n", mode, address, value); + //printf("mode=%c , address = 0x%04x, value = 0x%02x\n", mode, address, value); } if (ret < 0) { return ret; @@ -93,7 +92,6 @@ static int i2c_write_table_val(client interface i2c_master_if i2c, } - int imx219_read(client interface i2c_master_if i2c, uint16_t addr){ i2c_regop_res_t res; uint16_t val = i2c.read_reg16(IMX219_I2C_ADDR, addr, res); @@ -109,9 +107,7 @@ void imx219_read_gains(client interface i2c_master_if i2c, uint16_t values[5]){ values[4] = imx219_read(i2c, 0x015B); } - /// ------------------------------------------------------------------------------- - int imx219_init(client interface i2c_master_if i2c) { int ret = 0; @@ -184,6 +180,3 @@ int imx219_set_gain_dB(client interface i2c_master_if i2c, return ret; } - - -// https://github.com/torvalds/linux/blob/63355b9884b3d1677de6bd1517cd2b8a9bf53978/drivers/media/i2c/imx219.c#L993 From 0f5a4810e4b52627bce925fcfd76da59e672658c Mon Sep 17 00:00:00 2001 From: Aaron Stewart Date: Fri, 9 Jun 2023 15:16:17 -0400 Subject: [PATCH 069/306] Major change to camera API - Now the image data is passed from the camera component to the user app one line at a time. - It is now possible to get raw or decimated/demosaiced pixel data from the API in the same app - Simplifies the work being done on the camera thread --- camera/api/camera.h | 33 ++-- camera/api/packet_handler.h | 21 ++- camera/api/user_api.h | 53 ++++--- camera/src/camera.xc | 70 +-------- camera/src/packet_handler.c | 153 ++----------------- camera/src/user_api.c | 136 ++++++++++------- examples/take_picture_downsample/src/app.c | 8 +- examples/take_picture_downsample/src/app.h | 2 +- examples/take_picture_downsample/src/main.xc | 6 +- examples/take_picture_raw/src/app_raw.c | 23 ++- examples/take_picture_raw/src/app_raw.h | 2 +- examples/take_picture_raw/src/main.xc | 8 +- 12 files changed, 190 insertions(+), 325 deletions(-) diff --git a/camera/api/camera.h b/camera/api/camera.h index 3debabcc..ebd4b5ae 100644 --- a/camera/api/camera.h +++ b/camera/api/camera.h @@ -74,22 +74,21 @@ void camera_main( in port p_mipi_rxv, buffered in port:32 p_mipi_rxd, clock clk_mipi, - client interface i2c_master_if i2c, - streaming chanend c_user_api); - -/** - * Thread entry point for interfacing with the camera sensor. - * - * This version of the camera thread will capture raw data. - */ -void camera_main_raw( - tileref mipi_tile, - in port p_mipi_clk, - in port p_mipi_rxa, - in port p_mipi_rxv, - buffered in port:32 p_mipi_rxd, - clock clk_mipi, - client interface i2c_master_if i2c, - streaming chanend c_user_api); + client interface i2c_master_if i2c); + +// /** +// * Thread entry point for interfacing with the camera sensor. +// * +// * This version of the camera thread will capture raw data. +// */ +// void camera_main_raw( +// tileref mipi_tile, +// in port p_mipi_clk, +// in port p_mipi_rxa, +// in port p_mipi_rxv, +// buffered in port:32 p_mipi_rxd, +// clock clk_mipi, +// client interface i2c_master_if i2c, +// streaming chanend c_user_api); #endif //__XC__ diff --git a/camera/api/packet_handler.h b/camera/api/packet_handler.h index 9c53b048..54e12582 100644 --- a/camera/api/packet_handler.h +++ b/camera/api/packet_handler.h @@ -42,17 +42,16 @@ isp_params_t isp_params; void mipi_packet_handler( streaming_chanend_t c_pkt, streaming_chanend_t c_ctrl, - streaming_chanend_t c_out_row, - streaming_chanend_t c_user_api); - -/** - * Thread entry point for packet handling when capturing raw data. - */ -void mipi_packet_handler_raw( - streaming_chanend_t c_pkt, - streaming_chanend_t c_ctrl, - streaming_chanend_t c_out_row, - streaming_chanend_t c_user_api); + streaming_chanend_t c_out_row); + +// /** +// * Thread entry point for packet handling when capturing raw data. +// */ +// void mipi_packet_handler_raw( +// streaming_chanend_t c_pkt, +// streaming_chanend_t c_ctrl, +// streaming_chanend_t c_out_row, +// streaming_chanend_t c_user_api); #ifdef __XC__ } diff --git a/camera/api/user_api.h b/camera/api/user_api.h index 7ccfdb47..d6dbe01d 100644 --- a/camera/api/user_api.h +++ b/camera/api/user_api.h @@ -2,36 +2,41 @@ #include "xccompat.h" #include "sensor.h" -#define CH APP_IMAGE_CHANNEL_COUNT -#define H APP_IMAGE_HEIGHT_PIXELS -#define W APP_IMAGE_WIDTH_PIXELS +#define CH (APP_IMAGE_CHANNEL_COUNT) +#define H (APP_IMAGE_HEIGHT_PIXELS) +#define W (APP_IMAGE_WIDTH_PIXELS) -#define H_RAW MIPI_IMAGE_WIDTH_PIXELS -#define W_RAW MIPI_IMAGE_HEIGHT_PIXELS +#define H_RAW (MIPI_IMAGE_HEIGHT_PIXELS) +#define W_RAW (MIPI_IMAGE_WIDTH_PIXELS) // #define RAW_CAPTURE 0 -// Image structure -typedef struct { - int8_t pix[CH][H][W]; -} image_t; +#if defined(__XC__) || defined(__cplusplus) +extern "C" { +#endif -// User-side API -unsigned camera_capture_image( - int8_t image_buff[CH][H][W], - streaming_chanend_t c_cam_api); - -// Framework-side API -void camera_api_init(streaming_chanend_t c_api); -void camera_api_request_begin(); -void camera_api_request_complete(); -void camera_api_request_update( - const int8_t image_row[CH][W], +void camera_api_init(); + +void camera_api_new_row_raw( + const int8_t pixel_data[H_RAW], + const unsigned row_index); + +void camera_api_new_row_decimated( + const int8_t pixel_data[CH][W], const unsigned row_index); +unsigned camera_capture_row_raw( + int8_t pixel_data[H_RAW]); +unsigned camera_capture_row_decimated( + int8_t pixel_data[CH][W]); + +unsigned camera_capture_image_raw( + int8_t image_buff[H_RAW][W_RAW]); + +unsigned camera_capture_image( + int8_t image_buff[CH][H][W]); -// RAW -unsigned camera_capture_image_raw(int8_t image_buff[H_RAW * W_RAW], streaming_chanend_t c_cam_api); -void camera_api_request_update_raw(uint16_t line_number, uint8_t *img_row_ptr); -void camera_api_request_complete_raw(); +#if defined(__XC__) || defined(__cplusplus) +} +#endif \ No newline at end of file diff --git a/camera/src/camera.xc b/camera/src/camera.xc index f93e2104..84bd25f6 100644 --- a/camera/src/camera.xc +++ b/camera/src/camera.xc @@ -24,8 +24,7 @@ void camera_main( in port p_mipi_rxv, buffered in port:32 p_mipi_rxd, clock clk_mipi, - client interface i2c_master_if i2c, - streaming chanend c_user_api) + client interface i2c_master_if i2c) { streaming chan c_pkt; @@ -34,6 +33,8 @@ void camera_main( sensor_control_if sc_if; // chan c_sensor_control; + + camera_api_init(); // See AN for MIPI shim // 0x7E42 >> 0 1 1 1 1 1 1 001 000 010 // @@ -72,70 +73,7 @@ void camera_main( par { MipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); - mipi_packet_handler(c_pkt, c_ctrl, c_stat_thread, c_user_api); - statistics_thread(c_stat_thread, sc_if); - sensor_control(sc_if, i2c); - } -} - - - -void camera_main_raw( - tileref mipi_tile, - in port p_mipi_clk, - in port p_mipi_rxa, - in port p_mipi_rxv, - buffered in port:32 p_mipi_rxd, - clock clk_mipi, - client interface i2c_master_if i2c, - streaming chanend c_user_api) -{ - - streaming chan c_pkt; - streaming chan c_ctrl; - streaming chan c_stat_thread; - - sensor_control_if sc_if; - // chan c_sensor_control; - - // See AN for MIPI shim - // 0x7E42 >> 0111 1110 0100 0010 - // in the explorer BOARD DPDN is swap - write_node_config_reg(mipi_tile, - XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, - DEFAULT_MIPI_DPHY_CFG3); //TODO decompose into different values - - unsigned mipi_shim_cfg0 = MIPI_SHIM_CFG0_PACK(MIPI_SHIM_DEMUX_EN, - MIPI_SHIM_DEMUX_DATATYPE, - MIPI_SHIM_DEMUX_MODE, - MIPI_SHIM_STUFF_ENABLE, - 0); // disable bias - - // send packet to MIPI shim - MipiPacketRx_init(mipi_tile, - p_mipi_rxd, - p_mipi_rxv, - p_mipi_rxa, - p_mipi_clk, - clk_mipi, - mipi_shim_cfg0, - MIPI_CLK_DIV, - MIPI_CFG_CLK_DIV); - - // Start camera and its configurations - int r = 0; - r |= camera_init(i2c); - delay_milliseconds(100); - r |= camera_configure(i2c); - delay_milliseconds(600); - r |= camera_start(i2c); - delay_milliseconds(2000); - - // start the different jobs (packet controller, handler, and post_process) - par - { - MipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); - mipi_packet_handler_raw(c_pkt, c_ctrl, c_stat_thread, c_user_api); + mipi_packet_handler(c_pkt, c_ctrl, c_stat_thread); statistics_thread(c_stat_thread, sc_if); sensor_control(sc_if, i2c); } diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index b171866c..e21996e6 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -63,8 +63,6 @@ void handle_frame_start( image_vfilter_frame_init(&vfilter_accs[c][0]); } - - camera_api_request_begin(); } @@ -94,6 +92,9 @@ unsigned handle_pixel_data( int8_t output_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_WIDTH_PIXELS]) { + // First, service any raw requests. + camera_api_new_row_raw((int8_t*) &pkt->payload[0], ph_state.in_line_number); + // Bayer pattern is RGGB; even index rows have RG data, // odd index rows have GB data. unsigned pattern = ph_state.in_line_number % 2; @@ -164,8 +165,8 @@ void on_new_output_row( // Pass the output row along for statistics processing s_chan_out_word(c_out_row, (unsigned) &pix_out[0][0] ); - // Write image data to user buffer if there is one waiting - camera_api_request_update(pix_out, ph_state.out_line_number); + // Service and user requests for decimated output + camera_api_new_row_decimated(pix_out, ph_state.out_line_number); ph_state.out_line_number++; } @@ -188,36 +189,19 @@ void handle_frame_end( // Signal statistics thread to do frame-end work by sending NULL. s_chan_out_word(c_out_row, (unsigned) NULL); - - // If user is waiting for image, this signals that it's done. - camera_api_request_complete(); - - // printf("\n"); - // printf("out lines: %u\n", ph_state.out_line_number); - // printf("in lines: %u\n", ph_state.in_line_number); } -void handle_no_expected_lines(){ - if(ph_state.in_line_number >= SENSOR_RAW_IMAGE_HEIGHT_PIXELS){ - // We've received more lines of image data than we expected. - #ifdef ASSERT_ON_TOO_MANY_LINES - assert(0); - #endif - } -} - -// send the line number and the actual raw to the user api -void handle_expected_format_raw(const mipi_packet_t* pkt){ - camera_api_request_update_raw( - (uint16_t) ph_state.in_line_number, - (uint8_t *) &pkt->payload[0]); +void handle_no_expected_lines() +{ + if(ph_state.in_line_number >= SENSOR_RAW_IMAGE_HEIGHT_PIXELS){ + // We've received more lines of image data than we expected. +#ifdef ASSERT_ON_TOO_MANY_LINES + assert(0); +#endif + } } -// end of frame -void handle_frame_end_raw(){ - camera_api_request_complete_raw(); -} /** * Process a single packet. @@ -295,54 +279,6 @@ void handle_packet( } -static -void handle_packet_raw( - const mipi_packet_t* pkt, - streaming_chanend_t c_out_row) -{ - - // definitions - const mipi_header_t header = pkt->header; - const mipi_data_type_t data_type = MIPI_GET_DATA_TYPE(header); - - // At start-up we usually want to wait for a new frame before processing - // anything - if(ph_state.wait_for_frame_start - && data_type != MIPI_DT_FRAME_START) return; - - /* - The idea here is that logic that keeps the packet handler in a coherent - state, like tracking frame and line numbers, should go directly in here, but - logic that actually interprets, processes or reacts to packet data should go - into the individual functions. - */ - switch(data_type) - { - case MIPI_DT_FRAME_START: - ph_state.wait_for_frame_start = 0; - ph_state.in_line_number = 0; - ph_state.out_line_number = 0; - ph_state.frame_number++; - break; - - case MIPI_DT_FRAME_END: - handle_frame_end_raw(); - break; - - case MIPI_EXPECTED_FORMAT: - handle_no_expected_lines(); - handle_expected_format_raw(pkt); - - ph_state.in_line_number++; - break; - - default: - handle_unknown_packet(pkt); - break; - } -} - - /** * Top level of the packet handling thread. Receives MIPI packets from the * packet receiver and passes them to `handle_packet()` for parsing and @@ -351,8 +287,7 @@ void handle_packet_raw( void mipi_packet_handler( streaming_chanend_t c_pkt, streaming_chanend_t c_ctrl, - streaming_chanend_t c_out_row, - streaming_chanend_t c_user_api) + streaming_chanend_t c_out_row) { /* * These buffers will be used to hold received MIPI packets while they're @@ -362,7 +297,7 @@ void mipi_packet_handler( mipi_packet_t packet_buffer[MIPI_PKT_BUFFER_COUNT]; unsigned pkt_idx = 0; - camera_api_init(c_user_api); + camera_api_init(); // Give the MIPI packet receiver a first buffer s_chan_out_word(c_pkt, (unsigned) &packet_buffer[pkt_idx] ); @@ -385,61 +320,3 @@ void mipi_packet_handler( } } - - -/** - * Top level of the packet handling thread. Receives MIPI packets from the - * packet receiver and passes them to `handle_packet()` for parsing and - * processing. - */ -void mipi_packet_handler_raw( - streaming_chanend_t c_pkt, - streaming_chanend_t c_ctrl, - streaming_chanend_t c_out_row, - streaming_chanend_t c_user_api) -{ - /* - * These buffers will be used to hold received MIPI packets while they're - * being processed. - */ - __attribute__((aligned(8))) - mipi_packet_t packet_buffer[MIPI_PKT_BUFFER_COUNT]; - unsigned pkt_idx = 0; - - camera_api_init(c_user_api); - - // Give the MIPI packet receiver a first buffer - s_chan_out_word(c_pkt, (unsigned) &packet_buffer[pkt_idx] ); - - while(1) { - pkt_idx = (pkt_idx + 1) & (MIPI_PKT_BUFFER_COUNT-1); - - mipi_packet_t * pkt = (mipi_packet_t*) s_chan_in_word(c_pkt); - // Swap buffers with the receiver thread. Give it the next buffer - // to fill and take the last filled buffer from it. - s_chan_out_word(c_pkt, (unsigned) &packet_buffer[pkt_idx] ); - - // Process the packet - //const mipi_header_t header = pkt->header; - //const mipi_data_type_t data_type = MIPI_GET_DATA_TYPE(header); - - // unsigned time_start = measure_time(); - handle_packet_raw(pkt, c_out_row); - // unsigned time_proc = measure_time() - time_start; - } -} - - -// NOTES -//[1] -/* - // NOTE: If vertical filtering has to go into a separate thread, this buffer - // will need to be passed to another thread and must be done differently. - // NOTE: This buffer will only hold one color channel at a time because the - // horizontal filter separates color planes. - -[2] -// We don't need to watch for new rows from the vertical decimator here -// because we know a priori that blue will be the last one out. - -*/ diff --git a/camera/src/user_api.c b/camera/src/user_api.c index 5833a81c..903d071b 100644 --- a/camera/src/user_api.c +++ b/camera/src/user_api.c @@ -8,90 +8,124 @@ #include "utils.h" #include "user_api.h" -// Pointers to both downsampled or raw -static image_t *user_image; -static int8_t *image_raw_ptr; + +#define CHAN_RAW 0 +#define CHAN_DEC 1 // In order to interface the handler and api -streaming_chanend_t c_user_api; +streaming_channel_t c_user_api[2]; + -// ---------------------------------------------------------------- -void camera_api_init( - streaming_chanend_t c_api) +void camera_api_init() { - user_image = NULL; - c_user_api = c_api; + c_user_api[CHAN_RAW] = s_chan_alloc(); + c_user_api[CHAN_DEC] = s_chan_alloc(); } -void camera_api_request_update( - const int8_t image_row[CH][W], +void camera_api_new_row_raw( + const int8_t pixel_data[H_RAW], const unsigned row_index) { - if (user_image) - { - for (int k = 0; k < CH; k++) + int8_t* user_pixel_data; + + SELECT_RES( + CASE_THEN(c_user_api[CHAN_RAW].end_a, user_handler), + DEFAULT_THEN(default_handler)) { - c_memcpy((void*) &user_image->pix[k][row_index][0], - (void*) &image_row[k][0], - sizeof(int8_t) * W); + user_handler: + user_pixel_data = (int8_t*) s_chan_in_word(c_user_api[CHAN_RAW].end_a); + c_memcpy(user_pixel_data, (void*) pixel_data, W_RAW); + s_chan_out_word(c_user_api[CHAN_RAW].end_a, row_index); + break; + default_handler: + break; } - } } -void camera_api_request_begin() +void camera_api_new_row_decimated( + const int8_t pixel_data[CH][W], + const unsigned row_index) { - unsigned tmp; + int8_t* user_pixel_data; + SELECT_RES( - CASE_THEN(c_user_api, user_handler), + CASE_THEN(c_user_api[CHAN_DEC].end_a, user_handler), DEFAULT_THEN(default_handler)) { user_handler: - tmp = s_chan_in_word(c_user_api); - user_image = (image_t*) tmp; + user_pixel_data = (int8_t*) s_chan_in_word(c_user_api[CHAN_DEC].end_a); + c_memcpy(user_pixel_data, (void*) pixel_data, CH*W); + s_chan_out_word(c_user_api[CHAN_DEC].end_a, row_index); break; default_handler: break; } } -unsigned camera_capture_image( - int8_t image_buff[CH][H][W], - streaming_chanend_t c_cam_api) +unsigned camera_capture_row_raw( + int8_t pixel_data[H_RAW]) { - int8_t *p_image = &image_buff[0][0][0]; - - s_chan_out_word(c_cam_api, (unsigned)p_image); - return s_chan_in_word(c_cam_api); + s_chan_out_word(c_user_api[CHAN_RAW].end_b, (unsigned) &pixel_data[0]); + unsigned sdf = s_chan_in_word(c_user_api[CHAN_RAW].end_b); + return sdf; } -void camera_api_request_complete() + + +unsigned camera_capture_row_decimated( + int8_t pixel_data[CH][W]) { - if(user_image){ - s_chan_out_word(c_user_api, 1); - user_image = NULL; - } + s_chan_out_word(c_user_api[CHAN_DEC].end_b, (unsigned) &pixel_data[0][0]); + return s_chan_in_word(c_user_api[CHAN_DEC].end_b); } -// ---------------------------------------------------------------- unsigned camera_capture_image_raw( - int8_t image_buff[H_RAW*W_RAW], - streaming_chanend_t c_cam_api -) + int8_t image_buff[H_RAW][W_RAW]) { - image_raw_ptr = (int8_t*)&image_buff[0]; - c_user_api = c_cam_api; - unsigned tmp = s_chan_in_word(c_cam_api); - return tmp; -} + unsigned row_index; + + // Loop, capturing rows until we get one with row_index==0 + do { + row_index = camera_capture_row_raw(&image_buff[0][0]); + } while (row_index != 0); + + // Now capture the rest of the rows + for (unsigned i=1; i #include -void user_app(streaming_chanend_t c_cam_api){ +void user_app() +{ int8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; uint8_t out_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; @@ -14,7 +15,10 @@ void user_app(streaming_chanend_t c_cam_api){ // Wait for the image to set exposure delay_milliseconds(5000); printf("Requesting image...\n"); - camera_capture_image(image_buffer, c_cam_api); + if(camera_capture_image(image_buffer)){ + printf("Error capturing image\n"); + exit(1); + } printf("Image captured...\n"); // Rotate 180 degrees diff --git a/examples/take_picture_downsample/src/app.h b/examples/take_picture_downsample/src/app.h index d1a3078b..7419762c 100644 --- a/examples/take_picture_downsample/src/app.h +++ b/examples/take_picture_downsample/src/app.h @@ -4,4 +4,4 @@ #include "camera.h" -void user_app(streaming_chanend_t c_cam_api); +void user_app(); diff --git a/examples/take_picture_downsample/src/main.xc b/examples/take_picture_downsample/src/main.xc index be13aa72..b0f5c385 100644 --- a/examples/take_picture_downsample/src/main.xc +++ b/examples/take_picture_downsample/src/main.xc @@ -26,7 +26,6 @@ on tile[MIPI_TILE] : clock clk_mipi = MIPI_CLKBLK; int main(void) { - streaming chan c_cam_api; i2c_master_if i2c[1]; par { on tile[0]: i2c_master(i2c, 1, p_scl, p_sda, Kbps); @@ -37,10 +36,9 @@ int main(void) p_mipi_rxv, p_mipi_rxd, clk_mipi, - i2c[0], - c_cam_api); + i2c[0]); - on tile[MIPI_TILE]: user_app(c_cam_api); + on tile[MIPI_TILE]: user_app(); } return 0; } diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index c20bf9dc..89c0ea60 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -10,21 +10,34 @@ #include "app_raw.h" #include "io_utils.h" -void user_app_raw(streaming_chanend_t c_cam_api){ +void user_app_raw(){ // set the input image to 0 - int8_t image_buffer[H_RAW * W_RAW]; + int8_t image_buffer[H_RAW][W_RAW]; memset(image_buffer, -128, H_RAW * W_RAW); // wait for the camera to set I2C parameters - delay_milliseconds(2500); + delay_milliseconds(5000); // Request an image printf("Requesting image...\n"); - camera_capture_image_raw(image_buffer, c_cam_api); + if(camera_capture_image_raw(image_buffer)){ + printf("Error capturing image\n"); + exit(1); + } printf("Image captured...\n"); + + // Convert image from int8 to uint8 in-place + for (int i = 0; i < H_RAW; i++) { + for (int j = 0; j < W_RAW; j++) { + image_buffer[i][j] += 128; + } + } // Save the image to a file - write_image_file("capture.bin", (uint8_t * ) &image_buffer[0], MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES, 1); + write_image_file("capture.bin", (uint8_t * ) &image_buffer[0][0], + MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES, 1); + + printf("Image saved. Exiting.\n"); exit(0); } diff --git a/examples/take_picture_raw/src/app_raw.h b/examples/take_picture_raw/src/app_raw.h index 0ccad37e..4a9f6110 100644 --- a/examples/take_picture_raw/src/app_raw.h +++ b/examples/take_picture_raw/src/app_raw.h @@ -4,4 +4,4 @@ #include "camera.h" -void user_app_raw(streaming_chanend_t c_cam_api); +void user_app_raw(); diff --git a/examples/take_picture_raw/src/main.xc b/examples/take_picture_raw/src/main.xc index ca3afa7a..143b1b2c 100644 --- a/examples/take_picture_raw/src/main.xc +++ b/examples/take_picture_raw/src/main.xc @@ -29,24 +29,22 @@ on tile[MIPI_TILE] : clock clk_mipi = MIPI_CLKBLK; int main(void) { - streaming chan c_cam_api; i2c_master_if i2c[1]; chan xscope_chan; par { xscope_host_data(xscope_chan); on tile[0]: i2c_master(i2c, 1, p_scl, p_sda, Kbps); - on tile[MIPI_TILE]: camera_main_raw(tile[MIPI_TILE], + on tile[MIPI_TILE]: camera_main(tile[MIPI_TILE], p_mipi_clk, p_mipi_rxa, p_mipi_rxv, p_mipi_rxd, clk_mipi, - i2c[0], - c_cam_api); + i2c[0]); on tile[MIPI_TILE]: xscope_io_init(xscope_chan); - on tile[MIPI_TILE]: user_app_raw(c_cam_api); + on tile[MIPI_TILE]: user_app_raw(); } return 0; } From 52017bd8aceae41493ad4c374e9cfb2d2ac3b5f0 Mon Sep 17 00:00:00 2001 From: Aaron Stewart Date: Fri, 9 Jun 2023 15:21:24 -0400 Subject: [PATCH 070/306] Adds comments for camera api --- camera/api/user_api.h | 54 ++++++++++++++++++++++++++++++++++++++++++- camera/src/user_api.c | 4 ++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/camera/api/user_api.h b/camera/api/user_api.h index d6dbe01d..d084aa47 100644 --- a/camera/api/user_api.h +++ b/camera/api/user_api.h @@ -15,25 +15,77 @@ extern "C" { #endif +/** + * Initialize the camera API. Must be called before any other API functions. + */ void camera_api_init(); +/** + * SERVER SIDE + * + * Called by the packet handler when a new row of raw image data is + * available. Typically this will be Bayered image data. + */ void camera_api_new_row_raw( const int8_t pixel_data[H_RAW], const unsigned row_index); +/** + * SERVER SIDE + * + * Called by the packet handler when a new row of decimated image data is + * available. + */ void camera_api_new_row_decimated( const int8_t pixel_data[CH][W], const unsigned row_index); +/** + * CLIENT SIDE + * + * Called by the client to capture a row of raw image data. + * + * @param pixel_data The buffer to store the row of pixels in + * + * @return The row index of the captured row of pixels + */ unsigned camera_capture_row_raw( - int8_t pixel_data[H_RAW]); + int8_t pixel_data[W_RAW]); +/** + * CLIENT SIDE + * + * Called by the client to capture a row of decimated image data. + * + * @param pixel_data The buffer to store the row of pixels in + * + * @return The row index of the captured row of pixels + */ unsigned camera_capture_row_decimated( int8_t pixel_data[CH][W]); +/** + * CLIENT SIDE + * + * Called by the client to capture a raw image. + * + * @param image_buff The buffer to store the image in + * + * @return Returns 0 on success, non-zero on failure + */ unsigned camera_capture_image_raw( int8_t image_buff[H_RAW][W_RAW]); + +/** + * CLIENT SIDE + * + * Called by the client to capture a decimated image. + * + * @param image_buff The buffer to store the image in + * + * @return Returns 0 on success, non-zero on failure + */ unsigned camera_capture_image( int8_t image_buff[CH][H][W]); diff --git a/camera/src/user_api.c b/camera/src/user_api.c index 903d071b..e69ae1e2 100644 --- a/camera/src/user_api.c +++ b/camera/src/user_api.c @@ -23,7 +23,7 @@ void camera_api_init() } void camera_api_new_row_raw( - const int8_t pixel_data[H_RAW], + const int8_t pixel_data[W_RAW], const unsigned row_index) { int8_t* user_pixel_data; @@ -63,7 +63,7 @@ void camera_api_new_row_decimated( } unsigned camera_capture_row_raw( - int8_t pixel_data[H_RAW]) + int8_t pixel_data[W_RAW]) { s_chan_out_word(c_user_api[CHAN_RAW].end_b, (unsigned) &pixel_data[0]); unsigned sdf = s_chan_in_word(c_user_api[CHAN_RAW].end_b); From 7db03136fb6d950a51ec0c5409099972fcc8c032 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Mon, 12 Jun 2023 09:49:04 +0100 Subject: [PATCH 071/306] doc strcuture --- .../01_Introduction.rst | 0 .../02_Architecture_and_Design.rst | 0 .../03_Building_the_Software.rst | 23 ++++++++++++++++++ .../images/overviewdrawio.png | Bin .../index.rst | 0 doc/2_user_guide/.gitkeep | 0 doc/3_release_notes/.gitkeep | 0 7 files changed, 23 insertions(+) rename doc/{programming_guide => 1_programming_guide}/01_Introduction.rst (100%) rename doc/{programming_guide => 1_programming_guide}/02_Architecture_and_Design.rst (100%) create mode 100644 doc/1_programming_guide/03_Building_the_Software.rst rename doc/{programming_guide => 1_programming_guide}/images/overviewdrawio.png (100%) rename doc/{programming_guide => 1_programming_guide}/index.rst (100%) create mode 100644 doc/2_user_guide/.gitkeep create mode 100644 doc/3_release_notes/.gitkeep diff --git a/doc/programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst similarity index 100% rename from doc/programming_guide/01_Introduction.rst rename to doc/1_programming_guide/01_Introduction.rst diff --git a/doc/programming_guide/02_Architecture_and_Design.rst b/doc/1_programming_guide/02_Architecture_and_Design.rst similarity index 100% rename from doc/programming_guide/02_Architecture_and_Design.rst rename to doc/1_programming_guide/02_Architecture_and_Design.rst diff --git a/doc/1_programming_guide/03_Building_the_Software.rst b/doc/1_programming_guide/03_Building_the_Software.rst new file mode 100644 index 00000000..53e2264a --- /dev/null +++ b/doc/1_programming_guide/03_Building_the_Software.rst @@ -0,0 +1,23 @@ +Building the Software +======================= +This section will provide details on how the software is constructed. The basic steps and build requirements +can be found in the README.md file which is distributed with the source. + +.. contents:: Table of Contents + + +Requirements +------------------------------------------ + +Introduction +------------------------------------------ + +Bulding the firmware and the examples +------------------------------------------ + +Adding new files +------------------------------------------ + +Building the host app (xscope_fileio) +------------------------------------------ + diff --git a/doc/programming_guide/images/overviewdrawio.png b/doc/1_programming_guide/images/overviewdrawio.png similarity index 100% rename from doc/programming_guide/images/overviewdrawio.png rename to doc/1_programming_guide/images/overviewdrawio.png diff --git a/doc/programming_guide/index.rst b/doc/1_programming_guide/index.rst similarity index 100% rename from doc/programming_guide/index.rst rename to doc/1_programming_guide/index.rst diff --git a/doc/2_user_guide/.gitkeep b/doc/2_user_guide/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/doc/3_release_notes/.gitkeep b/doc/3_release_notes/.gitkeep new file mode 100644 index 00000000..e69de29b From 5f2043e42c17927a8b4bad64ad0805d7230af12e Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Mon, 12 Jun 2023 10:37:29 +0100 Subject: [PATCH 072/306] minor changes proposal --- camera/api/camera.h | 65 ++++++++++++++------------------- camera/api/isp.h | 12 ++++-- camera/api/packet_handler.h | 13 ++----- camera/src/camera.xc | 9 ++--- camera/src/packet_handler.c | 15 ++++---- modules/mipi/src/MipiPacketRx.S | 14 +++++-- 6 files changed, 60 insertions(+), 68 deletions(-) diff --git a/camera/api/camera.h b/camera/api/camera.h index ebd4b5ae..c245f519 100644 --- a/camera/api/camera.h +++ b/camera/api/camera.h @@ -25,29 +25,14 @@ #include "isp.h" -/// Indicates whether the demux is enabled on the MIPI shim -/// 0 = disabled, 1 = enabled -#define MIPI_SHIM_DEMUX_EN 0 -/// The MIPI datatype that is to be demuxed -/// Unused if MIPI_SHIM_DEMUX_EN = 0 -#define MIPI_SHIM_DEMUX_DATATYPE 0 -/// The MIPI shim demux mode (see xMIPI_DemuxMode_t) -/// Unused if MIPI_SHIM_DEMUX_EN = 0 -#define MIPI_SHIM_DEMUX_MODE 0 -/// Indicates whether the MIPI shim should apply a bias to received pixel data. -/// 0 = disabled, 1 = enabled (does not require MIPI_SHIM_DEMUX_EN = 1) -/// The bias subtracts 128 from each received pixel value, converting the -/// pixel data from uint8 to int8. -/// TODO: astew: This is currently ignored -- the bias is always enabled in the -/// downsample app, and disabled in the raw capture app. -#define MIPI_SHIM_BIAS_ENABLE 1 -/// Indicates whether the MIPI shim should stuff an extra byte into each triplet -/// of received pixel data, in order to achieve 4-byte alignment of pixel -/// values. -/// 0 = disabled, 1 = enabled (does not require MIPI_SHIM_DEMUX_EN = 1) -#define MIPI_SHIM_STUFF_ENABLE 0 // Enable byte stuffing - - +// MIPI Shim configuration register layout (MIPI_SHIM_CFG0) +#define MIPI_SHIM_BIAS_ENABLE 1 // Offset output pixels [1] +#define MIPI_SHIM_STUFF_ENABLE 0 // Add a zero byte after every RGB pixel [2] +#define MIPI_SHIM_DEMUX_MODE 0 // demux mode (see xMIPI_DemuxMode_t), Unused if MIPI_SHIM_DEMUX_EN = 0 +#define MIPI_SHIM_DEMUX_DATATYPE 0 // CSI-2 packet type to demux, Unused if MIPI_SHIM_DEMUX_EN = 0 +#define MIPI_SHIM_DEMUX_EN 0 // MIPI shim 0 = disabled, 1 = enabled + +// Mipi shim clock settings #define MIPI_CLK_DIV 1 // CLK DIVIDER #define MIPI_CFG_CLK_DIV 3 // CFG DIVIDER @@ -76,19 +61,25 @@ void camera_main( clock clk_mipi, client interface i2c_master_if i2c); -// /** -// * Thread entry point for interfacing with the camera sensor. -// * -// * This version of the camera thread will capture raw data. -// */ -// void camera_main_raw( -// tileref mipi_tile, -// in port p_mipi_clk, -// in port p_mipi_rxa, -// in port p_mipi_rxv, -// buffered in port:32 p_mipi_rxd, -// clock clk_mipi, -// client interface i2c_master_if i2c, -// streaming chanend c_user_api); #endif //__XC__ + + + +/* Notes + +[1] +Indicates whether the MIPI shim should apply a bias to received pixel data. +0 = disabled, 1 = enabled (does not require MIPI_SHIM_DEMUX_EN = 1) +The bias subtracts 128 from each received pixel value, converting the +pixel data from uint8 to int8. +TODO: astew: This is currently ignored -- the bias is always enabled in the + downsample app, and disabled in the raw capture app. + +[2] +Indicates whether the MIPI shim should stuff an extra byte into each triplet +of received pixel data, in order to achieve 4-byte alignment of pixel +values. +0 = disabled, 1 = enabled (does not require MIPI_SHIM_DEMUX_EN = 1) + +*/ \ No newline at end of file diff --git a/camera/api/isp.h b/camera/api/isp.h index 076c124e..671fd577 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -7,9 +7,6 @@ #include "statistics.h" -#define DEFAULT_ALFA 1.58682 -#define DEFAULT_BETA 1.52535 - // ---------------------------------- AE/AGC ------------------------------ float AE_compute_mean_skewness(global_stats_t *gstats); @@ -44,6 +41,15 @@ void isp_bilinear_resize( void isp_rotate_image(const uint8_t *src, uint8_t *dest, int width, int height); +// -------------------------- COLOR CONVERSION ------------------------------------- +// Macro arguments to get color components from packed result in the assembly program +#define GET_R(x) ((int8_t)((x & MASK24)) & MASK8) +#define GET_G(x) ((int8_t)((x & MASK24) >> 8) & MASK8) +#define GET_B(x) ((int8_t)((x & MASK24) >> 16) & MASK8) +#define GET_Y(x) GET_R(x) +#define GET_U(x) GET_G(x) +#define GET_V(x) GET_B(x) + int yuv_to_rgb( int y, int u, diff --git a/camera/api/packet_handler.h b/camera/api/packet_handler.h index 54e12582..2762219d 100644 --- a/camera/api/packet_handler.h +++ b/camera/api/packet_handler.h @@ -22,7 +22,9 @@ typedef struct } mipi_packet_t; -// struct to hold the calculated parameters for the ISP +/** + * struct to hold the calculated parameters for the ISP + */ typedef struct { float channel_gain[APP_IMAGE_CHANNEL_COUNT]; } isp_params_t; @@ -43,15 +45,6 @@ void mipi_packet_handler( streaming_chanend_t c_pkt, streaming_chanend_t c_ctrl, streaming_chanend_t c_out_row); - -// /** -// * Thread entry point for packet handling when capturing raw data. -// */ -// void mipi_packet_handler_raw( -// streaming_chanend_t c_pkt, -// streaming_chanend_t c_ctrl, -// streaming_chanend_t c_out_row, -// streaming_chanend_t c_user_api); #ifdef __XC__ } diff --git a/camera/src/camera.xc b/camera/src/camera.xc index 84bd25f6..5049a5aa 100644 --- a/camera/src/camera.xc +++ b/camera/src/camera.xc @@ -30,15 +30,12 @@ void camera_main( streaming chan c_pkt; streaming chan c_ctrl; streaming chan c_stat_thread; - sensor_control_if sc_if; - // chan c_sensor_control; + // set the channels for the camera api camera_api_init(); - // See AN for MIPI shim - // 0x7E42 >> 0 1 1 1 1 1 1 001 000 010 // - // Assigning lanes and polarities + // Assign lanes and polarities write_node_config_reg(mipi_tile, XS1_SSWITCH_MIPI_DPHY_CFG3_NUM, DEFAULT_MIPI_DPHY_CFG3); @@ -49,7 +46,7 @@ void camera_main( MIPI_SHIM_DEMUX_MODE, MIPI_SHIM_STUFF_ENABLE, 1); // enable bias - + // Initialize MIPI receiver MipiPacketRx_init(mipi_tile, p_mipi_rxd, p_mipi_rxv, diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index e21996e6..aa9a1faf 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -12,7 +12,13 @@ #include "utils.h" #include "sensor.h" +// Initial channel scales +#define AWB_gain_RED 1.3 +#define AWB_gain_GREEN 0.8 +#define AWB_gain_BLUE 1.3 +// Filter stride +#define HFILTER_INPUT_STRIDE (APP_DECIMATION_FACTOR) // State needed for the vertical filter static @@ -34,13 +40,6 @@ static struct { hfilter_state_t hfilter_state[APP_IMAGE_CHANNEL_COUNT]; -// Initial channel scales -#define AWB_gain_RED 1.3 -#define AWB_gain_GREEN 0.8 -#define AWB_gain_BLUE 1.3 -// #define AWB_gain_RED 1.0 -// #define AWB_gain_GREEN 1.0 -// #define AWB_gain_BLUE 1.0 isp_params_t isp_params = { .channel_gain = { @@ -76,7 +75,7 @@ void handle_unknown_packet( // 2 - error packets (in this case mipi reciever will raise an exception, but in the future we want to handle them here) } -#define HFILTER_INPUT_STRIDE (APP_DECIMATION_FACTOR) + /** * Handle a row of pixel data. diff --git a/modules/mipi/src/MipiPacketRx.S b/modules/mipi/src/MipiPacketRx.S index 3cd439fd..a0d8567c 100644 --- a/modules/mipi/src/MipiPacketRx.S +++ b/modules/mipi/src/MipiPacketRx.S @@ -2,7 +2,7 @@ #include #include -// XOR ENABLED +// XOR DISABLED // BYTEREV DISABLED /**************************************************** @@ -23,9 +23,15 @@ Then to receive a packet: - Return - Maybe it's a good idea to return the data type? - **************************************************** - **************************************************** - */ +**************************************************** +****************************************************/ + +/* +In case we need byte reversal or xor for some reason, here's how to do it: + // { byterev S0, S0 ; } + // { xor S0, S0, r8 ; } +After .L_RX_LONG_LOOP: +*/ /* See MIPI CSI section 9.1.2 From 5fb02afc279c4a8ae166b1bb321f9464e0e9f95e Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Mon, 12 Jun 2023 11:16:27 +0100 Subject: [PATCH 073/306] solving isp changes --- camera/api/isp.h | 13 +++++++------ tests/unit_tests/src/test/isp_test.c | 9 --------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/camera/api/isp.h b/camera/api/isp.h index 671fd577..2bb66099 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -43,12 +43,13 @@ void isp_rotate_image(const uint8_t *src, uint8_t *dest, int width, int height); // -------------------------- COLOR CONVERSION ------------------------------------- // Macro arguments to get color components from packed result in the assembly program -#define GET_R(x) ((int8_t)((x & MASK24)) & MASK8) -#define GET_G(x) ((int8_t)((x & MASK24) >> 8) & MASK8) -#define GET_B(x) ((int8_t)((x & MASK24) >> 16) & MASK8) -#define GET_Y(x) GET_R(x) -#define GET_U(x) GET_G(x) -#define GET_V(x) GET_B(x) +#define GET_R(rgb) ((rgb >> 16) & 0xFF) +#define GET_G(rgb) ((rgb >> 8) & 0xFF) +#define GET_B(rgb) (rgb & 0xFF) + +#define GET_Y(yuv) GET_R(yuv) +#define GET_U(yuv) GET_G(yuv) +#define GET_V(yuv) GET_B(yuv) int yuv_to_rgb( int y, diff --git a/tests/unit_tests/src/test/isp_test.c b/tests/unit_tests/src/test/isp_test.c index b3b837a8..47d0926e 100644 --- a/tests/unit_tests/src/test/isp_test.c +++ b/tests/unit_tests/src/test/isp_test.c @@ -33,15 +33,6 @@ typedef struct } color_table_t; -#define GET_R(rgb) ((rgb >> 16) & 0xFF) -#define GET_G(rgb) ((rgb >> 8) & 0xFF) -#define GET_B(rgb) (rgb & 0xFF) - -#define GET_Y(yuv) GET_R(yuv) -#define GET_U(yuv) GET_G(yuv) -#define GET_V(yuv) GET_B(yuv) - - void printColorTable(color_table_t* table) { printf("Color Table:\n"); printf("R: %d, G: %d, B: %d\n", table->R, table->G, table->B); From 5e521a5f27670fb69f1c507c2330e65f1ff96c55 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Mon, 12 Jun 2023 13:05:31 +0100 Subject: [PATCH 074/306] minor cleaning changes --- .gitignore | 1 + camera/api/user_api.h | 9 --------- camera/api/utils.h | 2 +- camera/src/utils.c | 13 +++++-------- examples/take_picture_raw/src/app_raw.c | 9 ++------- sensors/api/sensor.h | 7 +++++++ 6 files changed, 16 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index fc4ca980..1680b94a 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ *.swmem *.bin *.fs +*.exe # Test cruft **/.pytest_cache/* diff --git a/camera/api/user_api.h b/camera/api/user_api.h index d084aa47..8f505ac1 100644 --- a/camera/api/user_api.h +++ b/camera/api/user_api.h @@ -2,15 +2,6 @@ #include "xccompat.h" #include "sensor.h" -#define CH (APP_IMAGE_CHANNEL_COUNT) -#define H (APP_IMAGE_HEIGHT_PIXELS) -#define W (APP_IMAGE_WIDTH_PIXELS) - -#define H_RAW (MIPI_IMAGE_HEIGHT_PIXELS) -#define W_RAW (MIPI_IMAGE_WIDTH_PIXELS) - -// #define RAW_CAPTURE 0 - #if defined(__XC__) || defined(__cplusplus) extern "C" { #endif diff --git a/camera/api/utils.h b/camera/api/utils.h index c82678a3..3e534f82 100644 --- a/camera/api/utils.h +++ b/camera/api/utils.h @@ -30,7 +30,7 @@ void img_int8_to_uint8( uint8_t out_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS] ); -void img_int8_to_uint8_inplace(const size_t width, const size_t height, int8_t *image_buffer); +void img_int8_to_uint8_inplace(int8_t image_buffer[H_RAW][W_RAW]); #if defined(__XC__) || defined(__cplusplus) } diff --git a/camera/src/utils.c b/camera/src/utils.c index e1e0c4b5..c3f416b3 100644 --- a/camera/src/utils.c +++ b/camera/src/utils.c @@ -182,15 +182,12 @@ void img_int8_to_uint8( // Convert int8_t to uint8_t image -void img_int8_to_uint8_inplace( - const size_t width, - const size_t height, - int8_t* image_buffer -) +void img_int8_to_uint8_inplace(int8_t image_buffer[H_RAW][W_RAW]) { // Add 128 to all elements - for (size_t i = 0; i < width * height; i++) - { - image_buffer[i] = (uint8_t)(image_buffer[i] + 128); + for (int i = 0; i < H_RAW; i++) { + for (int j = 0; j < W_RAW; j++) { + image_buffer[i][j] += 128; + } } } diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index 89c0ea60..d362d7da 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -5,7 +5,6 @@ #include // user #include "mipi.h" -//#include "utils.h" #include "user_api.h" #include "app_raw.h" #include "io_utils.h" @@ -28,12 +27,8 @@ void user_app_raw(){ printf("Image captured...\n"); // Convert image from int8 to uint8 in-place - for (int i = 0; i < H_RAW; i++) { - for (int j = 0; j < W_RAW; j++) { - image_buffer[i][j] += 128; - } - } - + img_int8_to_uint8_inplace(image_buffer); + // Save the image to a file write_image_file("capture.bin", (uint8_t * ) &image_buffer[0][0], MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES, 1); diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index 685cfa61..fc58b819 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -161,5 +161,12 @@ #define APP_WB_PERCENTILE (0.95) #endif +// For simplicity here +#define CH (APP_IMAGE_CHANNEL_COUNT) +#define H (APP_IMAGE_HEIGHT_PIXELS) +#define W (APP_IMAGE_WIDTH_PIXELS) + +#define H_RAW (MIPI_IMAGE_HEIGHT_PIXELS) +#define W_RAW (MIPI_IMAGE_WIDTH_PIXELS) #endif // sensor_H From d4c4ce48c02b558e1d6c527c6eddc8a082ce8521 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Mon, 12 Jun 2023 14:47:27 +0100 Subject: [PATCH 075/306] removing xscope_close_all_files from the write APIs --- examples/take_picture_downsample/src/app.c | 5 ++- examples/take_picture_raw/src/app_raw.c | 1 + utils/io_utils/io_utils.c | 3 -- utils/io_utils/io_utils.h | 40 ++++++++++++++++++++++ 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index 4083ecef..4a5b18ba 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -27,7 +27,8 @@ void user_app() // convert to uint8 with right dimentions img_int8_to_uint8(image_buffer, temp_buffer); - uint8_t * io_buff = (uint8_t *) &image_buffer[0][0][0]; // io_buff this will have [APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS][APP_IMAGE_CHANNEL_COUNT] + uint8_t * io_buff = (uint8_t *) &image_buffer[0][0][0]; + // io_buff this will have [APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS][APP_IMAGE_CHANNEL_COUNT] dimentions swap_dimentions((uint8_t *) &temp_buffer[0][0][0], io_buff, APP_IMAGE_HEIGHT_PIXELS, APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_CHANNEL_COUNT); // Write binary file and .bmp file @@ -36,6 +37,8 @@ void user_app() //save it to bmp write_bmp_file("capture.bmp", io_buff, APP_IMAGE_HEIGHT_PIXELS, APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_CHANNEL_COUNT); + printf("Images saved. Exiting.\n"); + xscope_close_all_files(); // end here exit(0); } diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index d362d7da..51933b8e 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -34,5 +34,6 @@ void user_app_raw(){ MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES, 1); printf("Image saved. Exiting.\n"); + xscope_close_all_files(); exit(0); } diff --git a/utils/io_utils/io_utils.c b/utils/io_utils/io_utils.c index 43234554..134bba15 100644 --- a/utils/io_utils/io_utils.c +++ b/utils/io_utils/io_utils.c @@ -24,8 +24,6 @@ void write_file(char * filename, uint8_t * data, const size_t size) xscope_file_t fp = xscope_open_file(filename, "wb"); xscope_fwrite(&fp, data, size); - - xscope_close_all_files(); } void write_image_file(char * filename, uint8_t * image, const size_t height, const size_t width, const size_t channels) @@ -122,7 +120,6 @@ void write_bmp_file(char * filename, uint8_t * image, const size_t height, const } } - xscope_close_all_files(); printf("Image written into file: %s\n", filename); printf("Image dimentions: %d x %d\n", width, height); } diff --git a/utils/io_utils/io_utils.h b/utils/io_utils/io_utils.h index 9b4da060..f3a8d5d6 100644 --- a/utils/io_utils/io_utils.h +++ b/utils/io_utils/io_utils.h @@ -10,12 +10,52 @@ extern "C" { #include "xscope_io_device.h" +/** + * @brief Dumps data into a file + * + * @param filename Name of the file + * @param data Data to write + * @param size Size of the data + * @note The application has to end with xscope_close_all_files() + */ void write_file(char * filename, uint8_t * data, const size_t size); +/** + * @brief Swaps image dimentions from [channel][height][width] + * to [height][width][channel] + * + * @param image_in Input image + * @param image_out Output image + * @param height Image height + * @param width Image width + * @param channels Number of channels + */ void swap_dimentions(uint8_t * image_in, uint8_t * image_out, const size_t height, const size_t width, const size_t channels); +/** + * @brief Writes binary image file + * + * @param filename Name of the image + * @param image Pointer to the image data + * @param height Image height + * @param width Image width + * @param channels Number of channels + * @note Image has to be in [height][width][channel] format + * @note The application has to end with xscope_close_all_files() + */ void write_image_file(char * filename, uint8_t * image, const size_t height, const size_t width, const size_t channels); +/** + * @brief Writes bmp image file + * + * @param filename Name of the image + * @param image Pointer to the image data + * @param height Image height + * @param width Image width + * @param channels Number of channels + * @note Image has to be in [height][width][channel] format + * @note The application has to end with xscope_close_all_files() + */ void write_bmp_file(char * filename, uint8_t * image, const size_t height, const size_t width, const size_t channels); #ifdef __XC__ From 7797aa9d96c55ea7e634bac66fea4f90753be985 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Mon, 12 Jun 2023 14:52:34 +0100 Subject: [PATCH 076/306] removing old api --- camera/api/utils.h | 4 -- camera/src/utils.c | 123 --------------------------------------------- 2 files changed, 127 deletions(-) diff --git a/camera/api/utils.h b/camera/api/utils.h index 3e534f82..801a8b07 100644 --- a/camera/api/utils.h +++ b/camera/api/utils.h @@ -18,12 +18,8 @@ unsigned measure_time(){ return y; } -void write_image( - const char* filename, - uint8_t image[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); void c_memcpy(void *dst, void *src, size_t size); void rotate_image(const char *filename, uint8_t image[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); -void writeBMP(const char *filename, uint8_t img[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); void img_int8_to_uint8( int8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS], diff --git a/camera/src/utils.c b/camera/src/utils.c index c3f416b3..8ddc5952 100644 --- a/camera/src/utils.c +++ b/camera/src/utils.c @@ -8,33 +8,6 @@ #include "utils.h" -/** -* Write image to a binary file containing RGB data -* -* @param filename -Name of the image -* @param image - Image corresponding to a 3D array of uint8_t -*/ -void write_image( - const char* filename, - uint8_t image[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]) -{ - printf("Writing image...\n"); - static FILE* img_file = NULL; - img_file = fopen(filename, "wb"); - - - for(uint16_t k = 0; k < APP_IMAGE_HEIGHT_PIXELS; k++){ - for(uint16_t j = 0; j < APP_IMAGE_WIDTH_PIXELS; j++){ - for(uint8_t c = 0; c < APP_IMAGE_CHANNEL_COUNT; c++){ - fwrite(&image[c][k][j], sizeof(uint8_t), 1, img_file); - } - } - } - fclose(img_file); - printf("Outfile %s\n", filename); - printf("image size (%dx%d)\n", APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_HEIGHT_PIXELS); -} - // This is called when want to memcpy from Xc to C void c_memcpy( void* dst, @@ -67,102 +40,6 @@ void rotate_image( } } - -/** -* Writes BMP image to file. This function is used to write a bmp image to a file. -* -* @param filename - Name of file to write to. The file must end with. bmp -* @param img - Array of uint8_t that contains the image -*/ -void writeBMP(const char* filename, uint8_t img[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]) { - int width = APP_IMAGE_WIDTH_PIXELS; - int height = APP_IMAGE_HEIGHT_PIXELS; - int channels = APP_IMAGE_CHANNEL_COUNT; - // Define BMP file header and info header - unsigned char bmpFileHeader[14] = { - 'B', 'M', // Signature - 0, 0, 0, 0, // File size (to be filled later) - 0, 0, // Reserved - 0, 0, // Reserved - 54, 0, 0, 0 // Offset to image data - }; - - unsigned char bmpInfoHeader[40] = { - 40, 0, 0, 0, // Info header size - 0, 0, 0, 0, // Image width (to be filled later) - 0, 0, 0, 0, // Image height (to be filled later) - 1, 0, // Number of color planes - 8 * channels, 0, // Bits per pixel - 0, 0, 0, 0, // Compression method - 0, 0, 0, 0, // Image size (to be filled later) - 0, 0, 0, 0, // Horizontal resolution (pixel per meter) - 0, 0, 0, 0, // Vertical resolution (pixel per meter) - 0, 0, 0, 0, // Number of colors in the palette - 0, 0, 0, 0, // Number of important colors - }; - - // Calculate the row size (including padding) - int rowSize = width * channels; - int paddingSize = (4 - (rowSize % 4)) % 4; - int rowSizeWithPadding = rowSize + paddingSize; - - // Calculate the file size - int fileSize = 54 + (rowSizeWithPadding * height); - - // Update the file size in the BMP file header - bmpFileHeader[2] = (unsigned char)(fileSize); - bmpFileHeader[3] = (unsigned char)(fileSize >> 8); - bmpFileHeader[4] = (unsigned char)(fileSize >> 16); - bmpFileHeader[5] = (unsigned char)(fileSize >> 24); - - // Update the image width in the BMP info header - bmpInfoHeader[4] = (unsigned char)(width); - bmpInfoHeader[5] = (unsigned char)(width >> 8); - bmpInfoHeader[6] = (unsigned char)(width >> 16); - bmpInfoHeader[7] = (unsigned char)(width >> 24); - - // Update the image height in the BMP info header - bmpInfoHeader[8] = (unsigned char)(height); - bmpInfoHeader[9] = (unsigned char)(height >> 8); - bmpInfoHeader[10] = (unsigned char)(height >> 16); - bmpInfoHeader[11] = (unsigned char)(height >> 24); - - // Open the file for writing - FILE* file = fopen(filename, "wb"); - if (!file) { - printf("Error opening file for writing: %s\n", filename); - return; - } - - // Write the BMP file header and info header - fwrite(bmpFileHeader, sizeof(unsigned char), 14, file); - fwrite(bmpInfoHeader, sizeof(unsigned char), 40, file); - - // Write the image data row by row (with padding) - int i, j; - for (i = height - 1; i >= 0; i--) { - for (j = 0; j < width; j++) { - // Write the pixel data (assuming RGB order) - fwrite(&img[2][i][j], sizeof(unsigned char), 1, file); // Blue - fwrite(&img[1][i][j], sizeof(unsigned char), 1, file); // Green - fwrite(&img[0][i][j], sizeof(unsigned char), 1, file); // Red - // For 4-channel images, you can write the alpha channel here - // fwrite(&img[index + 3], sizeof(unsigned char), 1, file); // Alpha - } - - // Write the padding bytes - for (j = 0; j < paddingSize; j++) { - fwrite("\0", sizeof(unsigned char), 1, file); - } - } - - // Close the file - fclose(file); - printf("Outfile %s\n", filename); - printf("image size (%dx%d)\n", APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_HEIGHT_PIXELS); -} - - // Convert int8_t to uint8_t image void img_int8_to_uint8( int8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS], From 359468e77f5cdb411f33b1398216847a3242e848 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Mon, 12 Jun 2023 16:30:39 +0100 Subject: [PATCH 077/306] isp dynamic AWB and cleaning --- camera/api/isp.h | 29 +++++-- camera/api/packet_handler.h | 7 +- camera/src/camera.xc | 2 + camera/src/isp.c | 108 +++++++++++++++++-------- camera/src/packet_handler.c | 15 +--- camera/src/statistics.c | 50 +++++------- sensors/api/sensor.h | 4 +- sensors/imx219_reg.h | 152 ++++++++++++++++++++++++------------ 8 files changed, 224 insertions(+), 143 deletions(-) diff --git a/camera/api/isp.h b/camera/api/isp.h index 2bb66099..79a7a11c 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -8,21 +8,34 @@ #include "statistics.h" + // ---------------------------------- AE/AGC ------------------------------ +void AE_control_exposure( + global_stats_t *global_stats, + CLIENT_INTERFACE(sensor_control_if, sc_if)); +void AE_print_skewness(global_stats_t *gstats); float AE_compute_mean_skewness(global_stats_t *gstats); uint8_t AE_is_adjusted(float sk); uint8_t AE_compute_new_exposure(float exposure, float skewness); + // ---------------------------------- AWB ------------------------------ +// Initial channel scales +#define AWB_gain_RED 1.3 +#define AWB_gain_GREEN 0.8 +#define AWB_gain_BLUE 1.3 + +/** + * struct to hold the calculated parameters for the ISP + */ typedef struct { - float alfa; - float beta; - float gamma; -} AWB_gains_t; - -void AWB_compute_gains(global_stats_t *gstats, AWB_gains_t *gains); -void AWB_print_gains(AWB_gains_t *gains); -int8_t AWB_compute_filter_gain(int8_t coeff, float factor); + float channel_gain[APP_IMAGE_CHANNEL_COUNT]; +} isp_params_t; + + +extern isp_params_t isp_params; +void AWB_compute_gains(global_stats_t *gstats, isp_params_t *isp_params); +void AWB_print_gains(isp_params_t *isp_params); // ---------------------------------- GAMMA ------------------------------ diff --git a/camera/api/packet_handler.h b/camera/api/packet_handler.h index 2762219d..b8b57f78 100644 --- a/camera/api/packet_handler.h +++ b/camera/api/packet_handler.h @@ -7,6 +7,7 @@ #include "xccompat.h" #include "camera.h" +#include "isp.h" #ifdef __XC__ extern "C" { @@ -22,12 +23,6 @@ typedef struct } mipi_packet_t; -/** - * struct to hold the calculated parameters for the ISP - */ -typedef struct { - float channel_gain[APP_IMAGE_CHANNEL_COUNT]; -} isp_params_t; /** diff --git a/camera/src/camera.xc b/camera/src/camera.xc index 5049a5aa..faac8c53 100644 --- a/camera/src/camera.xc +++ b/camera/src/camera.xc @@ -64,6 +64,8 @@ void camera_main( r |= camera_configure(i2c); delay_milliseconds(600); r |= camera_start(i2c); + assert(r == 0); // assert that camera is started and configured + printf("\nCamera_started and configured...\n"); delay_milliseconds(2000); // start the different jobs (packet controller, handler, and post_process) diff --git a/camera/src/isp.c b/camera/src/isp.c index f53a1fab..85247ea7 100644 --- a/camera/src/isp.c +++ b/camera/src/isp.c @@ -1,7 +1,7 @@ #include "isp.h" -#define AE_MARGIN 0.1 // defaukt marging for the auto exposure error - +#define AE_MARGIN 0.1 // default marging for the auto exposure error +#define AE_INITIAL_EXPOSURE 35 // initial exposure value // ---------------------------------- utils ------------------------------ static @@ -10,6 +10,32 @@ uint8_t csign(float x) { } // ---------------------------------- AE / AGC ------------------------------ +void AE_control_exposure( + global_stats_t *global_stats, + CLIENT_INTERFACE(sensor_control_if, sc_if)) +{ + // Initial exposure + static uint8_t new_exp = AE_INITIAL_EXPOSURE; + float sk = AE_compute_mean_skewness(global_stats); + if (AE_is_adjusted(sk)){ + printf("-----> adjustement done\n"); + } + else{ + // adjust exposure + new_exp = AE_compute_new_exposure((float) new_exp, sk); + // printf("new exp = %d\n", new_exp); + sensor_control_set_exposure(sc_if, (uint8_t) new_exp); + } +} + + +void AE_print_skewness(global_stats_t *gstats){ + printf("skewness:%f,%f,%f\n", + (*gstats)[0].skewness, + (*gstats)[1].skewness, + (*gstats)[2].skewness); +} + float AE_compute_mean_skewness(global_stats_t *gstats){ float sk = 0.0; sk += (*gstats)[0].skewness; @@ -58,42 +84,60 @@ uint8_t AE_compute_new_exposure(float exposure, float skewness) // ---------------------------------- AWB ------------------------------ -void AWB_compute_gains(global_stats_t *gstats, AWB_gains_t *gains){ - // Adjust AWB - const float ceil = 254.0; - gains->alfa = ceil / (float)(*gstats)[0].percentile; // RED - gains->beta = ceil / (float)(*gstats)[1].percentile; // GREEN - gains->gamma = ceil / (float)(*gstats)[2].percentile; // BLUE -} +const float AWB_ceil = 254.0; +const float AWB_MAX = 1.6; +const float AWB_MIN = 0.8; + +isp_params_t isp_params = { + .channel_gain = { + AWB_gain_RED, + AWB_gain_GREEN, + AWB_gain_BLUE + } +}; -void AWB_print_gains(AWB_gains_t *gains){ - printf("awb:%f,%f,%f\n",gains->alfa,gains->beta,gains->gamma); +static +float AWB_clip_value(float tmp){ + if (tmp > AWB_MAX){ + tmp = AWB_MAX; + } + else if (tmp < AWB_MIN){ + tmp = AWB_MIN; + } + return tmp; } -int8_t AWB_compute_filter_gain(int8_t coeff, float factor) { - // clip factor - const float maxf = 1.6; - const float minf = 1; - if (factor > maxf){ - factor = maxf; - } - else if (factor <= minf){ - factor = minf; - } - - // compute the factor - float result = factor * (coeff + 128.0f); - if (result >= 255.0f) { - result = 127; - } else if (result <= 0.0f) { - result = -128; - } else{ - result -= 128; - } - return (int8_t)result; +void AWB_compute_gains(global_stats_t *gstats, isp_params_t *isp_params){ + // Adjust AWB + float tmp1, tmp2, tmp3; + + tmp1 = AWB_ceil / (float)(*gstats)[0].percentile; // RED + tmp2 = AWB_ceil / (float)(*gstats)[1].percentile; // GREEN + tmp3 = AWB_ceil / (float)(*gstats)[2].percentile; // BLUE + + // Proportional control with saturation to the white reference + tmp1 = 0.2*(1.3-tmp1) + 1.3; + tmp2 = 0.2*(1-tmp2) + 1; + tmp3 = 0.2*(1.3-tmp3) + 1.3; + + tmp1 = AWB_clip_value(tmp1); + tmp2 = AWB_clip_value(tmp2); + tmp3 = AWB_clip_value(tmp3); + + isp_params->channel_gain[0] = tmp1; + isp_params->channel_gain[1] = tmp2; + isp_params->channel_gain[2] = tmp3; } +void AWB_print_gains(isp_params_t *isp_params){ + printf("awb:%f,%f,%f\n", + isp_params->channel_gain[0], + isp_params->channel_gain[1], + isp_params->channel_gain[2]); +} + + // ---------------------------------- GAMMA ------------------------------ /** diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index aa9a1faf..d2ce85a0 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -12,10 +12,6 @@ #include "utils.h" #include "sensor.h" -// Initial channel scales -#define AWB_gain_RED 1.3 -#define AWB_gain_GREEN 0.8 -#define AWB_gain_BLUE 1.3 // Filter stride #define HFILTER_INPUT_STRIDE (APP_DECIMATION_FACTOR) @@ -40,22 +36,13 @@ static struct { hfilter_state_t hfilter_state[APP_IMAGE_CHANNEL_COUNT]; - -isp_params_t isp_params = { - .channel_gain = { - AWB_gain_RED, - AWB_gain_GREEN, - AWB_gain_BLUE - } -}; - - static void handle_frame_start( const mipi_packet_t* pkt) { // New frame is starting, reset the vertical filter accumulator states. for(int c = 0; c < APP_IMAGE_CHANNEL_COUNT; c++){ + // printf("isp params = %f\n", isp_params.channel_gain[c]); pixel_hfilter_update_scale(&hfilter_state[c], isp_params.channel_gain[c], (c == 0)? 0 : 1); diff --git a/camera/src/statistics.c b/camera/src/statistics.c index e1908fd0..4fdcc856 100644 --- a/camera/src/statistics.c +++ b/camera/src/statistics.c @@ -12,14 +12,14 @@ #define HISTOGRAM_TOTAL_SAMPLES (HISTOGRAM_SAMPLE_PER_ROW * APP_IMAGE_HEIGHT_PIXELS ) // This is the normalization factor static const float histogram_norm_factor = (1.0 / (float) HISTOGRAM_TOTAL_SAMPLES); -// Initial exposure -uint8_t new_exp = 35; - /** - * //TODO - */ +* Update histogram based on pixel values. +* +* @param hist - * pointer to the histogram to update. Must be large enough to accommodate the number of pixels in the image. +* @param pix - array of pixel values to update the histogram with +*/ void update_histogram( channel_histogram_t* hist, const int8_t pix[]) @@ -65,6 +65,7 @@ void compute_skewness(channel_stats_t *stats) + /** * Compute simple statistics for a set of data. * @param stats - * Pointer to the channel statistics to be computed @@ -106,7 +107,12 @@ void find_percentile(channel_stats_t *stats, const float fraction) } -// Main thread for the statistics +/** +* Thread that computes statistics for each pixel in the image. +* The statistics are stored in a struct which is used to perform +* isp corrections +* @param c_img_in - Channel end of the +*/ void statistics_thread( streaming_chanend_t c_img_in, CLIENT_INTERFACE(sensor_control_if, sc_if)) @@ -115,7 +121,6 @@ void statistics_thread( while(1){ // Declare new stats global_stats_t global_stats = {{0}}; - AWB_gains_t awb_gains = {0}; // Inner loop iterates over rows within a frame while(1){ @@ -134,37 +139,22 @@ void statistics_thread( } } - // End of frame + // End of frame, compute statistics for(uint8_t channel = 0; channel < APP_IMAGE_CHANNEL_COUNT; channel++){ compute_skewness(&global_stats[channel]); compute_simple_stats(&global_stats[channel]); find_percentile(&global_stats[channel], APP_WB_PERCENTILE); } - // TODO delete this os leave it as a function - printf("skewness:%f,%f,%f\n", - global_stats[0].skewness, - global_stats[1].skewness, - global_stats[2].skewness); - - float sk = AE_compute_mean_skewness(&global_stats); - if (AE_is_adjusted(sk)){ - printf("-----> adjustement done\n"); - } - else{ - // adjust exposure - new_exp = AE_compute_new_exposure((float) new_exp, sk); - printf("new exp = %d\n", new_exp); - sensor_control_set_exposure(sc_if, (uint8_t) new_exp); - } + // Adjust AE + AE_control_exposure(&global_stats, sc_if); // Adjust AWB - AWB_compute_gains(&global_stats, &awb_gains); - AWB_print_gains(&awb_gains); - // Apply gains - //RED_GAIN = 1.4;//= awb_gains.alfa; - //GREEN_GAIN = awb_gains.beta; - //BLUE_GAIN = awb_gains.gamma; + AWB_compute_gains(&global_stats, &isp_params); + + // Print ISP info + AWB_print_gains(&isp_params); + AE_print_skewness(&global_stats); } } diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index fc58b819..2f0bd5ce 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -64,10 +64,10 @@ #ifndef CONFIG_MIPI_FORMAT #error CONFIG_MIPI_FORMAT has to be specified #else - #if CONFIG_MIPI_FORMAT == MIPI_DT_RAW10 + #if (CONFIG_MIPI_FORMAT == MIPI_DT_RAW10) #define MIPI_IMAGE_WIDTH_BYTES (((MIPI_IMAGE_WIDTH_PIXELS) >> 2) * 5) // by 5/4 - #elif CONFIG_MIPI_FORMAT == MIPI_DT_RAW8 + #elif (CONFIG_MIPI_FORMAT == MIPI_DT_RAW8) #define MIPI_IMAGE_WIDTH_BYTES MIPI_IMAGE_WIDTH_PIXELS // same size #else diff --git a/sensors/imx219_reg.h b/sensors/imx219_reg.h index e2966cbe..b48ba295 100644 --- a/sensors/imx219_reg.h +++ b/sensors/imx219_reg.h @@ -96,57 +96,107 @@ static imx219_settings_t imx219_lanes_regs[] = { }; - -static imx219_settings_t mode_640_480_regs[] = { - {0x0164, 0x03}, - {0x0165, 0xe8}, - {0x0166, 0x08}, - {0x0167, 0xe7}, - {0x0168, 0x02}, - {0x0169, 0xf0}, - {0x016a, 0x06}, - {0x016b, 0xaf}, - {0x016c, 0x02}, - {0x016d, 0x80}, - {0x016e, 0x01}, - {0x016f, 0xe0}, - {0x0624, 0x06}, - {0x0625, 0x68}, - {0x0626, 0x04}, - {0x0627, 0xd0}, -}; - -static imx219_settings_t mode_1640_1232_regs[] = { - {0x0164, 0x00}, - {0x0165, 0x00}, - {0x0166, 0x0c}, - {0x0167, 0xcf}, - {0x0168, 0x00}, - {0x0169, 0x00}, - {0x016a, 0x09}, - {0x016b, 0x9f}, - {0x016c, 0x06}, - {0x016d, 0x68}, - {0x016e, 0x04}, - {0x016f, 0xd0}, - {0x0624, 0x06}, - {0x0625, 0x68}, - {0x0626, 0x04}, - {0x0627, 0xd0}, -}; - - -static imx219_settings_t raw10_framefmt_regs[] = { - {0x018c, 0x0a}, - {0x018d, 0x0a}, - {0x0309, 0x0a}, -}; - -static imx219_settings_t raw8_framefmt_regs[] = { - {0x018c, 0x08}, - {0x018d, 0x08}, - {0x0309, 0x08}, -}; +#if (CONFIG_MODE == MODE_VGA_640x480) + static imx219_settings_t mode_640_480_regs[] = { + {0x0164, 0x03}, + {0x0165, 0xe8}, + {0x0166, 0x08}, + {0x0167, 0xe7}, + {0x0168, 0x02}, + {0x0169, 0xf0}, + {0x016a, 0x06}, + {0x016b, 0xaf}, + {0x016c, 0x02}, + {0x016d, 0x80}, + {0x016e, 0x01}, + {0x016f, 0xe0}, + {0x0624, 0x06}, + {0x0625, 0x68}, + {0x0626, 0x04}, + {0x0627, 0xd0}, + }; +#endif + +#if (CONFIG_MODE == MODE_UXGA_1640x1232) + static imx219_settings_t mode_1640_1232_regs[] = { + {0x0164, 0x00}, + {0x0165, 0x00}, + {0x0166, 0x0c}, + {0x0167, 0xcf}, + {0x0168, 0x00}, + {0x0169, 0x00}, + {0x016a, 0x09}, + {0x016b, 0x9f}, + {0x016c, 0x06}, + {0x016d, 0x68}, + {0x016e, 0x04}, + {0x016f, 0xd0}, + {0x0624, 0x06}, + {0x0625, 0x68}, + {0x0626, 0x04}, + {0x0627, 0xd0}, + }; +#endif + + +#if (CONFIG_MODE == MODE_FHD_1920x1080) + static imx219_settings_t mode_1920_1080_regs[] = { + {0x0164, 0x02}, + {0x0165, 0xa8}, + {0x0166, 0x0a}, + {0x0167, 0x27}, + {0x0168, 0x02}, + {0x0169, 0xb4}, + {0x016a, 0x06}, + {0x016b, 0xeb}, + {0x016c, 0x07}, + {0x016d, 0x80}, + {0x016e, 0x04}, + {0x016f, 0x38}, + {0x0624, 0x07}, + {0x0625, 0x80}, + {0x0626, 0x04}, + {0x0627, 0x38}, + }; +#endif + + +#if (CONFIG_MODE == MODE_WQSXGA_3280x2464) + static imx219_settings_t mode_3280x2464_regs[] = { + {0x0164, 0x00}, + {0x0165, 0x00}, + {0x0166, 0x0c}, + {0x0167, 0xcf}, + {0x0168, 0x00}, + {0x0169, 0x00}, + {0x016a, 0x09}, + {0x016b, 0x9f}, + {0x016c, 0x0c}, + {0x016d, 0xd0}, + {0x016e, 0x09}, + {0x016f, 0xa0}, + {0x0624, 0x0c}, + {0x0625, 0xd0}, + {0x0626, 0x09}, + {0x0627, 0xa0}, + }; +#endif + +#if (CONFIG_MIPI_FORMAT == MIPI_DT_RAW10) + static imx219_settings_t raw10_framefmt_regs[] = { + {0x018c, 0x0a}, + {0x018d, 0x0a}, + {0x0309, 0x0a}, + }; +#elif (CONFIG_MIPI_FORMAT == MIPI_DT_RAW8) + static imx219_settings_t raw8_framefmt_regs[] = { + {0x018c, 0x08}, + {0x018d, 0x08}, + {0x0309, 0x08}, + }; +#else + #error CONFIG_MIPI_FORMAT not supported +#endif static imx219_settings_t binning_regs[] = { From cd0fd14fe823b30c7b0178225d8a089b93a883ff Mon Sep 17 00:00:00 2001 From: Aaron Stewart Date: Mon, 12 Jun 2023 11:30:50 -0400 Subject: [PATCH 078/306] Adds unit tests for pixel_vfilter_*() functions --- camera/api/image_vfilter.h | 4 +- camera/src/asm/pixel_vfilter_acc_init.S | 4 +- tests/unit_tests/src/main.c | 1 + .../unit_tests/src/test/pixel_hfilter_test.c | 29 +- .../unit_tests/src/test/pixel_vfilter_test.c | 593 ++++++++++++++++++ 5 files changed, 618 insertions(+), 13 deletions(-) create mode 100644 tests/unit_tests/src/test/pixel_vfilter_test.c diff --git a/camera/api/image_vfilter.h b/camera/api/image_vfilter.h index 962cad2a..3b12c415 100644 --- a/camera/api/image_vfilter.h +++ b/camera/api/image_vfilter.h @@ -6,9 +6,11 @@ #include "sensor.h" - +// The number of non-zero taps in the vertical filter. #define VFILTER_TAP_COUNT (5) +// The effective decimation factor of the vertical filter. +// Note that because of Bayering, this is half the horizontal decimation factor. #define VFILTER_DEC_FACTOR (APP_DECIMATION_FACTOR / 2) // Required: Low-res image width must be multiple of 16 pixels diff --git a/camera/src/asm/pixel_vfilter_acc_init.S b/camera/src/asm/pixel_vfilter_acc_init.S index 30a330da..0d250bfb 100644 --- a/camera/src/asm/pixel_vfilter_acc_init.S +++ b/camera/src/asm/pixel_vfilter_acc_init.S @@ -62,12 +62,12 @@ FUNCTION_NAME: std tmp, tmp, r11[1] std tmp, tmp, r11[2] std tmp, tmp, r11[3] -{ shr r5, len, 4 ; vldr r11[0] } +{ shr r5, len, 4 ; vldd r11[0] } std value, value, r11[0] std value, value, r11[1] std value, value, r11[2] std value, value, r11[3] -{ zext len, 4 ; vldd r11[0] } +{ zext len, 4 ; vldr r11[0] } // for now, assume the len is multiple of 16 and raise an exception if not. // Part of the problem with supporting a tail is that if it is supported here diff --git a/tests/unit_tests/src/main.c b/tests/unit_tests/src/main.c index e3ee6521..80139168 100644 --- a/tests/unit_tests/src/main.c +++ b/tests/unit_tests/src/main.c @@ -18,6 +18,7 @@ int main( printf("\n"); RUN_TEST_GROUP(pixel_hfilter); + RUN_TEST_GROUP(pixel_vfilter); RUN_TEST_GROUP(isp_tests); return UNITY_END(); diff --git a/tests/unit_tests/src/test/pixel_hfilter_test.c b/tests/unit_tests/src/test/pixel_hfilter_test.c index 0c051b38..6c38de34 100644 --- a/tests/unit_tests/src/test/pixel_hfilter_test.c +++ b/tests/unit_tests/src/test/pixel_hfilter_test.c @@ -228,23 +228,31 @@ TEST(pixel_hfilter, pixel_hfilter__alt_coef) /////////////////////////////////////////////// TEST(pixel_hfilter, pixel_hfilter__timing) { + static const unsigned max_blocks = 8; + int8_t coef[32] = {0}; int8_t input[32] = {0}; - int8_t output[64] = {0}; + int8_t output[16*max_blocks] = {0}; + + unsigned timing[max_blocks]; - unsigned t_16x[4]; + static const char func_name[] = "pixel_hfilter()"; + static const char row1_head[] = "out_count:"; + static const char row2_head[] = "ticks: "; - for(int k = 0; k < 4; k++){ + for(int k = 0; k < max_blocks; k++){ unsigned ts = measure_time(); pixel_hfilter(output, input, coef, 0, 0 , 0, 16*(k+1)); unsigned te = measure_time(); - t_16x[k] = te - ts; + timing[k] = te - ts; } - printf("\n"); - printf("\tout_count: 16 32 48 64\n"); - printf("\tticks: %8u %8u %8u %8u\n", - t_16x[0], t_16x[1], t_16x[2], t_16x[3]); + printf("\n\t%s timing:\n", func_name); + printf("\t\t%s", row1_head); + for(int k = 0; k < max_blocks; k++) printf("%8u", 16*(k+1)); + printf("\n\t\t%s", row2_head); + for(int k = 0; k < max_blocks; k++) printf("%8u", timing[k]); + printf("\n\n"); } @@ -364,6 +372,7 @@ TEST(pixel_hfilter, pixel_hfilter_update_scale__timing) pixel_hfilter_update_scale(&state, gain, 1); unsigned te = measure_time(); - printf("\n"); - printf("\tpixel_hfilter_update_scale: %u ticks\n", te - ts); + static const char func_name[] = "pixel_hfilter_update_scale()"; + + printf("\n\t%s timing: %u ticks\n\n", func_name, te - ts); } diff --git a/tests/unit_tests/src/test/pixel_vfilter_test.c b/tests/unit_tests/src/test/pixel_vfilter_test.c new file mode 100644 index 00000000..0717fe29 --- /dev/null +++ b/tests/unit_tests/src/test/pixel_vfilter_test.c @@ -0,0 +1,593 @@ +// Copyright 2020-2022 XMOS LIMITED. +// This Software is subject to the terms of the XMOS Public Licence: Version 1. + +#include +#include +#include +#include +#include +#include + +#include "unity_fixture.h" + +#include "camera.h" + +TEST_GROUP_RUNNER(pixel_vfilter) { + RUN_TEST_CASE(pixel_vfilter, pixel_vfilter_acc_init__case0); + RUN_TEST_CASE(pixel_vfilter, pixel_vfilter_acc_init__case1); + RUN_TEST_CASE(pixel_vfilter, pixel_vfilter_acc_init__timing); + + RUN_TEST_CASE(pixel_vfilter, pixel_vfilter_complete__case0); + RUN_TEST_CASE(pixel_vfilter, pixel_vfilter_complete__case1); + RUN_TEST_CASE(pixel_vfilter, pixel_vfilter_complete__timing); + + RUN_TEST_CASE(pixel_vfilter, pixel_vfilter_macc__case0); + RUN_TEST_CASE(pixel_vfilter, pixel_vfilter_macc__case1); + RUN_TEST_CASE(pixel_vfilter, pixel_vfilter_macc__case2); + RUN_TEST_CASE(pixel_vfilter, pixel_vfilter_macc__case3); + RUN_TEST_CASE(pixel_vfilter, pixel_vfilter_macc__timing); +} + +#define ACC_HI(X) (((X)>>16)&0xFFFF) +#define ACC_LO(X) (((X)>> 0)&0xFFFF) + + + +static +int debug_iter = -1; + + +TEST_GROUP(pixel_vfilter); +TEST_SETUP(pixel_vfilter) { + fflush(stdout); + debug_iter = -1; +} + +TEST_TEAR_DOWN(pixel_vfilter) { + if(debug_iter != -1){ + printf("\nDEBUG:\n"); + printf("\tdebug_iter = %d\n", debug_iter); + } + debug_iter = -1; +} + + + + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(pixel_vfilter, pixel_vfilter_acc_init__case0) +{ + static const unsigned acc_blocks = 1; + + struct { + uint32_t pre_padding; + struct { + int16_t hi[16]; + int16_t lo[16]; + } accs[acc_blocks]; + uint32_t post_padding; + } thing; + + // These ensure that the function doesn't write outside of the bounds of the + // array + thing.pre_padding = 0x52C6ABE2; + thing.post_padding = 0xABCD1234; + + static const + int32_t test_cases[] = { + 0x00000000, 0x00000001, 0x00010000, -0x00000001, 0x12345678, + }; + + static const + unsigned n_cases = sizeof(test_cases) / sizeof(test_cases[0]); + + for(int iter = 0; iter < n_cases; iter++){ + debug_iter = iter; // For debug on failure + + const int32_t acc_value = test_cases[iter]; + + memset(&thing.accs, 0, sizeof(thing.accs)); + + pixel_vfilter_acc_init(&thing.accs[0].hi[0], acc_value, 16*acc_blocks); + + for(int k = 0; k < acc_blocks; k++){ + for(int i = 0; i < 16; i++){ + TEST_ASSERT_EQUAL_INT16(ACC_HI(acc_value), thing.accs[k].hi[i]); + TEST_ASSERT_EQUAL_INT16(ACC_LO(acc_value), thing.accs[k].lo[i]); + } + } + TEST_ASSERT_EQUAL_UINT32(0x52C6ABE2, thing.pre_padding); + TEST_ASSERT_EQUAL_UINT32(0xABCD1234, thing.post_padding); + } + + debug_iter = -1; // signals not to print debug on teardown +} + + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(pixel_vfilter, pixel_vfilter_acc_init__case1) +{ + static const unsigned acc_blocks = 4; + + struct { + uint32_t pre_padding; + struct { + int16_t hi[16]; + int16_t lo[16]; + } accs[acc_blocks]; + uint32_t post_padding; + } thing; + + // These ensure that the function doesn't write outside of the bounds of the + // array + thing.pre_padding = 0x52C6ABE2; + thing.post_padding = 0xABCD1234; + + if(1){ + const int32_t acc_value = 0x12345678; + memset(&thing.accs, 0, sizeof(thing.accs)); + pixel_vfilter_acc_init(&thing.accs[0].hi[0], acc_value, 16*acc_blocks); + for(int k = 0; k < acc_blocks; k++){ + for(int i = 0; i < 16; i++){ + TEST_ASSERT_EQUAL_INT16(ACC_HI(acc_value), thing.accs[k].hi[i]); + TEST_ASSERT_EQUAL_INT16(ACC_LO(acc_value), thing.accs[k].lo[i]); + } + } + TEST_ASSERT_EQUAL_UINT32(0x52C6ABE2, thing.pre_padding); + TEST_ASSERT_EQUAL_UINT32(0xABCD1234, thing.post_padding); + } +} + + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(pixel_vfilter, pixel_vfilter_acc_init__timing) +{ + static const unsigned acc_blocks = 8; + + int32_t accs[16*acc_blocks]; + + unsigned timing[acc_blocks]; + + static const char func_name[] = "pixel_vfilter_acc_init()"; + static const char row1_head[] = "out_count:"; + static const char row2_head[] = "ticks: "; + + for(int k = 0; k < acc_blocks; k++){ + unsigned ts = measure_time(); + pixel_vfilter_acc_init((int16_t*) &accs, 0, 16*(k+1)); + unsigned te = measure_time(); + timing[k] = te - ts; + } + + printf("\n\t%s timing:\n", func_name); + printf("\t\t%s", row1_head); + for(int k = 0; k < acc_blocks; k++) printf("%8u", 16*(k+1)); + printf("\n\t\t%s", row2_head); + for(int k = 0; k < acc_blocks; k++) printf("%8u", timing[k]); + printf("\n\n"); +} + + + + + + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(pixel_vfilter, pixel_vfilter_complete__case0) +{ + static const unsigned acc_per_vec = 16; + static const unsigned acc_blocks = 1; + static const unsigned output_pixels = acc_blocks*acc_per_vec; + + struct { + int16_t hi[acc_per_vec]; + int16_t lo[acc_per_vec]; + } accs[acc_blocks]; + + int8_t output[output_pixels]; + int16_t shifts[acc_per_vec]; + + int8_t expected_out[output_pixels] = {0}; + + struct { + int32_t acc_value; + int16_t shift; + int8_t expected_out; + } test_cases[] = { + // accumulator shift output + { 0x00000000, 0, 0x00}, + { 0x00000001, 0, 0x01}, + { -0x00000001, 0, -0x01}, + { 0x00000002, 1, 0x01}, + { 0x00000004, 1, 0x02}, + { 0x00000004, 2, 0x01}, + { -0x00000004, 1, -0x02}, + { 0x0000007F, 0, 0x7F}, + { -0x0000007F, 0, -0x7F}, + { -0x00000080, 0, -0x7F}, + { 0x00000100, 0, 0x7F}, + { 0x007E0000, 16, 0x7E}, + }; + + static const unsigned n_cases = sizeof(test_cases)/sizeof(test_cases[0]); + + for(int iter = 0; iter < n_cases; iter++){ + debug_iter = iter; // For debug on failure + + const int32_t acc_value = test_cases[iter].acc_value; + + for(int k = 0; k < acc_per_vec; k++) + shifts[k] = test_cases[iter].shift; + + for(int k = 0; k < output_pixels; k++) + expected_out[k] = test_cases[iter].expected_out; + + pixel_vfilter_acc_init(&accs[0].hi[0], acc_value, output_pixels); + + pixel_vfilter_complete(output, &accs[0].hi[0], shifts, output_pixels); + + TEST_ASSERT_EQUAL_INT8_ARRAY(expected_out, output, output_pixels); + } + + debug_iter = -1; // signals not to print debug on teardown +} + + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(pixel_vfilter, pixel_vfilter_complete__case1) +{ + static const unsigned acc_per_vec = 16; + static const unsigned acc_blocks = 6; + static const unsigned output_pixels = acc_blocks*acc_per_vec; + + struct { + int16_t hi[acc_per_vec]; + int16_t lo[acc_per_vec]; + } accs[acc_blocks]; + + pixel_vfilter_acc_init(&accs[0].hi[0], 0x007E0000, output_pixels); + + int16_t shifts[acc_per_vec]; + for(int k = 0; k < acc_per_vec; k++) shifts[k] = 16; + + // Add 16 bytes of padding around either side of the array to ensure that the + // function doesn't write outside of the bounds. + int8_t output[output_pixels + 32]; + int8_t expected_out[output_pixels + 32]; + + const int8_t expected = 0x7E; + + for(int blocks = 1; blocks < acc_blocks; blocks++){ + debug_iter = blocks; // For debug on failure + + memset(output, 0, sizeof(output)); + memset(expected_out, 0, sizeof(expected_out)); + + for(int k = 0; k < 16*blocks; k++) + expected_out[k+16] = expected; + + pixel_vfilter_complete(&output[16], &accs[0].hi[0], shifts, 16*blocks); + + TEST_ASSERT_EQUAL_INT8_ARRAY(expected_out, output, output_pixels + 32); + } + + debug_iter = -1; // signals not to print debug on teardown +} + + + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(pixel_vfilter, pixel_vfilter_complete__timing) +{ + static const unsigned max_blocks = 8; + + int32_t accs[16*max_blocks]; + int8_t output[16*max_blocks]; + int16_t shifts[16] = {0}; + + unsigned timing[max_blocks]; + + static const char func_name[] = "pixel_vfilter_complete()"; + static const char row1_head[] = "out_count:"; + static const char row2_head[] = "ticks: "; + + for(int k = 0; k < max_blocks; k++){ + unsigned ts = measure_time(); + pixel_vfilter_complete(output, (int16_t*) &accs, shifts, 16*(k+1)); + unsigned te = measure_time(); + timing[k] = te - ts; + } + + printf("\n\t%s timing:\n", func_name); + printf("\t\t%s", row1_head); + for(int k = 0; k < max_blocks; k++) printf("%8u", 16*(k+1)); + printf("\n\t\t%s", row2_head); + for(int k = 0; k < max_blocks; k++) printf("%8u", timing[k]); + printf("\n\n"); +} + + + + + + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(pixel_vfilter, pixel_vfilter_macc__case0) +{ + static const unsigned acc_per_vec = 16; + static const unsigned acc_blocks = 1; + static const unsigned out_count = acc_per_vec*acc_blocks; + + int32_t accs[out_count]; + int8_t coef[16]; + int8_t pixels_in[out_count]; + + int32_t expected_accs[out_count]; + + struct { + int32_t acc_init; + int8_t coef; + int8_t pixel; + int32_t expected_acc; + } test_case[] = { + // acc_init, coef, pixel, expected, + { 0x00000000, 0x00, 0x00, 0x00000000, }, + { 0x00000001, 0x00, 0x00, 0x00000001, }, + { 0x00001234, 0x00, 0x01, 0x00001234, }, + { 0x00001234, 0x01, 0x00, 0x00001234, }, + { 0x00001234, 0x01, 0x01, 0x00001235, }, + { -0x00001234, 0x01, 0x02, -0x00001232, }, + { 0x00010000, 0x0A, 0x10, 0x000100A0, }, + }; + + static const + unsigned n_cases = sizeof(test_case)/sizeof(test_case[0]); + + for(int iter = 0; iter < n_cases; iter++){ + debug_iter = iter; // For debug on failure + + int32_t acc_init = test_case[iter].acc_init; + int8_t coeff = test_case[iter].coef; + int8_t pixel = test_case[iter].pixel; + int32_t expected_acc = test_case[iter].expected_acc; + + pixel_vfilter_acc_init((int16_t*) &accs[0], acc_init, out_count); + pixel_vfilter_acc_init((int16_t*) &expected_accs[0], expected_acc, out_count); + + memset(coef, coeff, sizeof(coef)); + memset(pixels_in, pixel, sizeof(pixels_in)); + + pixel_vfilter_macc((int16_t*) &accs[0], pixels_in, coef, out_count); + + TEST_ASSERT_EQUAL_INT32_ARRAY(expected_accs, accs, out_count); + + } + + debug_iter = -1; // signals not to print debug on teardown +} + + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(pixel_vfilter, pixel_vfilter_macc__case1) +{ + static const unsigned acc_per_vec = 16; + static const unsigned max_blocks = 4; + static const unsigned max_out_count = acc_per_vec*max_blocks; + + int32_t accs[max_out_count]; + int8_t coef[16]; + int8_t pixels_in[max_out_count]; + + int32_t expected_accs[max_out_count]; + + + for(int blocks = 1; blocks <= max_blocks; blocks++){ + debug_iter = blocks-1; // For debug on failure + + unsigned out_count = acc_per_vec*blocks; + + int32_t acc_init = 0x00010000; + int8_t coeff = 0x05; + int8_t pixel = 0x20; + int32_t expected_acc = 0x000100A0; + + memset(accs, 0, sizeof(accs)); + memset(expected_accs, 0, sizeof(expected_accs)); + pixel_vfilter_acc_init((int16_t*) &accs[0], acc_init, out_count); + pixel_vfilter_acc_init((int16_t*) &expected_accs[0], expected_acc, out_count); + + memset(coef, coeff, sizeof(coef)); + memset(pixels_in, pixel, sizeof(pixels_in)); + + pixel_vfilter_macc((int16_t*) &accs[0], pixels_in, coef, out_count); + + // Compare all the way to end to make sure it doesn't write past end + TEST_ASSERT_EQUAL_INT32_ARRAY(expected_accs, accs, max_out_count); + + } + + debug_iter = -1; // signals not to print debug on teardown +} + + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(pixel_vfilter, pixel_vfilter_macc__case2) +{ + static const unsigned acc_per_vec = 16; + + int16_t accs[2][acc_per_vec]; + int8_t coef[acc_per_vec]; + int8_t pixels_in[acc_per_vec]; + + int32_t expected_accs[acc_per_vec]; + + int32_t result_accs[acc_per_vec]; + + struct { + int32_t acc_init; + int8_t coef; + int8_t pixel; + } test_case[] = { + // acc_init, coef, pixel + { 0x00000000, 0x00, 0x00}, + { 0x00000001, 0x00, 0x00}, + { 0x00001234, 0x00, 0x01}, + { 0x00001234, 0x01, 0x00}, + { 0x00001234, 0x01, 0x01}, + { -0x00001234, 0x01, 0x02}, + { 0x00010000, 0x0A, 0x10}, + }; + + static const + unsigned n_cases = sizeof(test_case)/sizeof(test_case[0]); + + for(int iter = 0; iter < n_cases; iter++){ + debug_iter = iter; // For debug on failure + + int32_t acc_init = test_case[iter].acc_init; + int8_t coeff = test_case[iter].coef; + int8_t pixel = test_case[iter].pixel; + + pixel_vfilter_acc_init(&accs[0][0], acc_init, acc_per_vec); + + memset(coef, coeff, sizeof(coef)); + for(int k = 0; k < acc_per_vec; k++) + pixels_in[k] = pixel + k; + + for(int k = 0; k < acc_per_vec; k++) + expected_accs[k] = acc_init + coeff * (pixel + k); + + pixel_vfilter_macc((int16_t*) &accs[0], pixels_in, coef, acc_per_vec); + + for(int k = 0; k < acc_per_vec; k++){ + result_accs[k] = (accs[0][k] << 16) | (accs[1][k]); + } + + TEST_ASSERT_EQUAL_INT32_ARRAY(expected_accs, result_accs, acc_per_vec); + + } + + debug_iter = -1; // signals not to print debug on teardown +} + + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(pixel_vfilter, pixel_vfilter_macc__case3) +{ + static const unsigned acc_per_vec = 16; + + int16_t accs[2][acc_per_vec]; + int8_t coef[acc_per_vec]; + int8_t pixels_in[acc_per_vec]; + + int32_t expected_accs[acc_per_vec]; + + int32_t result_accs[acc_per_vec]; + + struct { + int32_t acc_init; + int8_t coef; + int8_t pixel; + } test_case[] = { + // acc_init, coef, pixel + { 0x00000000, 0x00, 0x00}, + { 0x00000001, 0x00, 0x00}, + { 0x00001234, 0x00, 0x01}, + { 0x00001234, 0x01, 0x00}, + { 0x00001234, 0x01, 0x01}, + { -0x00001234, 0x01, 0x02}, + { 0x00010000, 0x0A, 0x10}, + }; + + static const + unsigned n_cases = sizeof(test_case)/sizeof(test_case[0]); + + for(int iter = 0; iter < n_cases; iter++){ + debug_iter = iter; // For debug on failure + + int32_t acc_init = test_case[iter].acc_init; + int8_t coeff = test_case[iter].coef; + int8_t pixel = test_case[iter].pixel; + + for(int k = 0; k < acc_per_vec; k++){ + accs[0][k] = ACC_HI(acc_init+7*k); + accs[1][k] = ACC_LO(acc_init+7*k); + } + + memset(coef, coeff, sizeof(coef)); + for(int k = 0; k < acc_per_vec; k++) + pixels_in[k] = pixel + k; + + for(int k = 0; k < acc_per_vec; k++) + expected_accs[k] = acc_init + 7*k + coeff * (pixel + k); + + pixel_vfilter_macc((int16_t*) &accs[0], pixels_in, coef, acc_per_vec); + + for(int k = 0; k < acc_per_vec; k++){ + result_accs[k] = (accs[0][k] << 16) | (accs[1][k]); + } + + TEST_ASSERT_EQUAL_INT32_ARRAY(expected_accs, result_accs, acc_per_vec); + + } + + debug_iter = -1; // signals not to print debug on teardown +} + + +/////////////////////////////////////////////// +/////////////////////////////////////////////// +/////////////////////////////////////////////// +TEST(pixel_vfilter, pixel_vfilter_macc__timing) +{ + static const unsigned acc_per_vec = 16; + static const unsigned max_blocks = 8; + static const unsigned max_out_count = acc_per_vec*max_blocks; + + int32_t accs[max_out_count]; + int8_t coef[16]; + int8_t pixels_in[max_out_count]; + + unsigned timing[max_blocks]; + + static const char func_name[] = "pixel_vfilter_macc()"; + static const char row1_head[] = "out_count:"; + static const char row2_head[] = "ticks: "; + + for(int k = 0; k < max_blocks; k++){ + unsigned blocks = k+1; + unsigned out_count = acc_per_vec*blocks; + + unsigned ts = measure_time(); + pixel_vfilter_macc((int16_t*) &accs[0], pixels_in, coef, out_count); + unsigned te = measure_time(); + timing[k] = te - ts; + } + + printf("\n\t%s timing:\n", func_name); + printf("\t\t%s", row1_head); + for(int k = 0; k < max_blocks; k++) printf("%8u", 16*(k+1)); + printf("\n\t\t%s", row2_head); + for(int k = 0; k < max_blocks; k++) printf("%8u", timing[k]); + printf("\n\n"); +} \ No newline at end of file From 9d98c6ec1d162bff36ff951b31cd7b0fecb192e3 Mon Sep 17 00:00:00 2001 From: Aaron Stewart Date: Mon, 12 Jun 2023 15:08:56 -0400 Subject: [PATCH 079/306] Modifies API to allow cropped image capture. Cleans up some util functions. --- camera/api/image_vfilter.h | 91 +++++++- camera/api/user_api.h | 33 +++ camera/api/utils.h | 57 ++++- camera/src/user_api.c | 42 ++++ camera/src/utils.c | 251 ++++++++++----------- examples/take_picture_downsample/src/app.c | 18 +- examples/take_picture_raw/src/app_raw.c | 4 +- 7 files changed, 349 insertions(+), 147 deletions(-) diff --git a/camera/api/image_vfilter.h b/camera/api/image_vfilter.h index 3b12c415..1b3924ab 100644 --- a/camera/api/image_vfilter.h +++ b/camera/api/image_vfilter.h @@ -36,30 +36,109 @@ extern "C" { #endif -typedef struct { - int next_tap; - int16_t buff[VFILTER_ACC_WIDTH_SHORTS]; -} vfilter_acc_t; - +/** + * Initialize a vector of 32-bit split accumulators to a given value. + * + * This function is used to initialize a set of accumulators to an offset value + * to prepare for filtering. + * + * The `pixel_vfilter_*()` functions use the VPU in 8-bit mode. All accumulators + * are initialized to the same 32-bit value, with the values split as required + * by the VPU. + * + * Accordingly, `pix_count` must be a multiple of 16. + * + * @param accs The vector of accumulators to initialize. + * @param acc_value The value to initialize the accumulators to. + * @param pix_count The number of accumulators to initialize. + */ void pixel_vfilter_acc_init( int16_t* accs, const int32_t acc_value, const unsigned pix_count); +/** + * Generate output pixel values from a vector of accumulators. + * + * This function is to be called on a row of accumulators after the final filter + * tap has been applied. The 32-bit accumulators are right-shifted by the values + * in `shifts[]` (with rounding), saturated to 8-bit symmetric bounds, and + * dropped from int32_t to int8_t before being written to `pix_out[]`. + * + * `pix_out[]`, `accs` and `shifts` must each be aligned to a 4-byte boundary. + * + * `accs` is expected to be formatted according to the VPU's split 32-bit + * accumulator format. + * + * `shifts[k]` will be applied to accumulators with indices `k mod 16`. Usually, + * each `shift[k]` will be the same. + * + * Due to VPU limitations, `pix_count` must be a multiple of 16. + * + * `pix_count` bytes will be written to `pix_out[]`. + * + * @param pix_out The output pixel values. + * @param accs The vector of accumulators to generate output from. + * @param shifts The right-shifts to apply to each accumulator. + * @param pix_count The number of output pixels. + */ void pixel_vfilter_complete( int8_t* pix_out, const int16_t* accs, const int16_t shifts[16], const unsigned pix_count); + +/** + * Apply a filter tap to a vector of accumulators using the supplied input + * pixels. + * + * This function is used to apply a single filter tap to a vector of + * accumulators (i.e. `pix_count` separate filters are updated). + * + * If `ACC32[k]` is the 32-bit accumulator associated with output pixel `k`, + * then this function computes: + * + * ACC32[k] += (pix_in[k] * filter[k % 16]) + * + * Because this function is used for incrementally applying the same vertical + * filter across many columns of an image, the values `filter[k]` will typically + * all be the same value, `COEF` for a given call to this function, simplifying + * to: + * + * ACC32[k] += (pix_in[k] * COEF) + * + * The `pix_in[]` array is expected to contain `pix_count` 8-bit pixels. + * + * `accs` is expected to be formatted according to the VPU's split 32-bit + * accumulator format. + * + * `accs`, `pix_in` and `filter[]` must each be 4-byte aligned. + * + * Due to VPU limitations, `pix_count` must be a multiple of 16. + * + * When the final filter tap is applied, `pixel_vfilter_complete()` should be + * called to generate output pixels from the accumulators. + * + * @param accs The vector of accumulators to apply the filter tap to. + * @param pix_in The input pixels to apply to the filter. + * @param filter Vector containing filter coefficients. + * @param pix_count The number of pixels to apply the filter to. + */ void pixel_vfilter_macc( int16_t* accs, const int8_t* pix_in, const int8_t filter[16], - const unsigned length_bytes); + const unsigned pix_count); + +typedef struct { + int next_tap; + int16_t buff[VFILTER_ACC_WIDTH_SHORTS]; +} vfilter_acc_t; + void image_vfilter_frame_init( vfilter_acc_t accs[]); diff --git a/camera/api/user_api.h b/camera/api/user_api.h index 8f505ac1..4d06faad 100644 --- a/camera/api/user_api.h +++ b/camera/api/user_api.h @@ -7,6 +7,8 @@ extern "C" { #endif /** + * SERVER SIDE + * * Initialize the camera API. Must be called before any other API functions. */ void camera_api_init(); @@ -80,6 +82,37 @@ unsigned camera_capture_image_raw( unsigned camera_capture_image( int8_t image_buff[CH][H][W]); + +typedef struct { + struct { + unsigned row; + unsigned col; + } origin; + struct { + unsigned height; + unsigned width; + } shape; +} image_crop_params_t; + +/** + * CLIENT SIDE + * + * Called by the client to capture a portion of a decimated image. If only a + * portion of the decimated image is required, using this function avoids the + * need to store the entire decimated image in memory. + * + * `image_buff` must be a 3D array of + * size `[CH][crop_params.shape.height][crop_params.shape.width]`. + * + * @param image_buff The buffer to store the image in + * @param crop_params The parameters of the crop + * + * @return Returns 0 on success, non-zero on failure + */ +unsigned camera_capture_image_cropped( + int8_t* image_buff, + const image_crop_params_t crop_params); + #if defined(__XC__) || defined(__cplusplus) } #endif \ No newline at end of file diff --git a/camera/api/utils.h b/camera/api/utils.h index 3e534f82..9389250e 100644 --- a/camera/api/utils.h +++ b/camera/api/utils.h @@ -18,19 +18,60 @@ unsigned measure_time(){ return y; } +/** +* Writes BMP image to file. This function is used to write a bmp image to a +* file. +* +* @param filename - Name of file to write to. The file must end with. bmp +* @param img - Array of uint8_t that contains the image +* @param height - Height of the image +* @param width - Width of the image +*/ +void writeBMP( + const char* filename, + uint8_t img[], + const unsigned height, + const unsigned width); + + +/** +* Write image to a binary file containing RGB data +* +* @param filename -Name of the image +* @param image - Image corresponding to a 3D array of uint8_t +* @param channels - Number of channels in the image +* @param height - Height of the image +* @param width - Width of the image +*/ void write_image( const char* filename, - uint8_t image[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); + uint8_t image[], + const unsigned channels, + const unsigned height, + const unsigned width); + + void c_memcpy(void *dst, void *src, size_t size); -void rotate_image(const char *filename, uint8_t image[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); -void writeBMP(const char *filename, uint8_t img[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); -void img_int8_to_uint8( - int8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS], - uint8_t out_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS] -); +void rotate_image( + const char *filename, + uint8_t image[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); + + -void img_int8_to_uint8_inplace(int8_t image_buffer[H_RAW][W_RAW]); +/** + * Convert an array of int8 to an array of uint8. + * + * Data can be updated in-place. + * + * @param output - Array of uint8_t that will contain the output + * @param input - Array of int8_t that contains the input + * @param length - Length of the input and output arrays + */ +void vect_int8_to_uint8( + uint8_t output[], + int8_t input[], + const unsigned length); #if defined(__XC__) || defined(__cplusplus) } diff --git a/camera/src/user_api.c b/camera/src/user_api.c index e69ae1e2..b7384712 100644 --- a/camera/src/user_api.c +++ b/camera/src/user_api.c @@ -129,3 +129,45 @@ unsigned camera_capture_image( return 0; } + + + +unsigned camera_capture_image_cropped( + int8_t* image_buff, + const image_crop_params_t crop_params) +{ + const unsigned CROP_ROW = crop_params.origin.row; + const unsigned CROP_COL = crop_params.origin.col; + const unsigned CROP_H = crop_params.shape.height; + const unsigned CROP_W = crop_params.shape.width; + + unsigned row_index; + + int8_t pixel_data[CH][W]; + + int8_t (*image)[CROP_H][CROP_W] = + (int8_t (*)[CROP_H][CROP_W]) image_buff; + + // Loop, capturing rows until we get one + // with row_index==crop_params.origin.row + do { + row_index = camera_capture_row_decimated(pixel_data); + } while (row_index != CROP_ROW); + + for(int c = 0; c < CH; c++) + c_memcpy(&image[c][0][0], &pixel_data[c][CROP_COL], CROP_W); + + // Now capture the rest of the rows + for (unsigned row = 1; row < CROP_H; row++) { + row_index = camera_capture_row_decimated(pixel_data); + + // TODO handle errors better + if (row_index != row + crop_params.origin.row) return 1; + + for(int c = 0; c < CH; c++) + c_memcpy(&image[c][row][0], &pixel_data[c][CROP_COL], CROP_W); + + } + + return 0; +} diff --git a/camera/src/utils.c b/camera/src/utils.c index c3f416b3..11ad47eb 100644 --- a/camera/src/utils.c +++ b/camera/src/utils.c @@ -7,34 +7,34 @@ #include "utils.h" - -/** -* Write image to a binary file containing RGB data -* -* @param filename -Name of the image -* @param image - Image corresponding to a 3D array of uint8_t -*/ void write_image( const char* filename, - uint8_t image[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]) + uint8_t image[], + const unsigned channels, + const unsigned height, + const unsigned width) { printf("Writing image...\n"); + + int8_t (*img3d)[height][width] = (int8_t (*)[height][width]) image; + static FILE* img_file = NULL; img_file = fopen(filename, "wb"); - - for(uint16_t k = 0; k < APP_IMAGE_HEIGHT_PIXELS; k++){ - for(uint16_t j = 0; j < APP_IMAGE_WIDTH_PIXELS; j++){ - for(uint8_t c = 0; c < APP_IMAGE_CHANNEL_COUNT; c++){ - fwrite(&image[c][k][j], sizeof(uint8_t), 1, img_file); + for(uint16_t k = 0; k < height; k++){ + for(uint16_t j = 0; j < width; j++){ + for(uint8_t c = 0; c < channels; c++){ + fwrite(&img3d[c][k][j], sizeof(uint8_t), 1, img_file); } } } fclose(img_file); printf("Outfile %s\n", filename); - printf("image size (%dx%d)\n", APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_HEIGHT_PIXELS); + printf("image size (%dx%d)\n", width, height); } + + // This is called when want to memcpy from Xc to C void c_memcpy( void* dst, @@ -67,127 +67,124 @@ void rotate_image( } } +static +int writeBMPHeader( + FILE* file, + const unsigned height, + const unsigned width) +{ + int channels = APP_IMAGE_CHANNEL_COUNT; + + // Define BMP file header and info header + unsigned char bmpFileHeader[14] = { + 'B', 'M', // Signature + 0, 0, 0, 0, // File size (to be filled later) + 0, 0, // Reserved + 0, 0, // Reserved + 54, 0, 0, 0 // Offset to image data + }; + + unsigned char bmpInfoHeader[40] = { + 40, 0, 0, 0, // Info header size + 0, 0, 0, 0, // Image width (to be filled later) + 0, 0, 0, 0, // Image height (to be filled later) + 1, 0, // Number of color planes + 8 * channels, 0, // Bits per pixel + 0, 0, 0, 0, // Compression method + 0, 0, 0, 0, // Image size (to be filled later) + 0, 0, 0, 0, // Horizontal resolution (pixel per meter) + 0, 0, 0, 0, // Vertical resolution (pixel per meter) + 0, 0, 0, 0, // Number of colors in the palette + 0, 0, 0, 0, // Number of important colors + }; + + // Calculate the row size (including padding) + int rowSize = width * channels; + int paddingSize = (4 - (rowSize % 4)) % 4; + int rowSizeWithPadding = rowSize + paddingSize; + + // Calculate the file size + int fileSize = 54 + (rowSizeWithPadding * height); + + // Update the file size in the BMP file header + bmpFileHeader[2] = (unsigned char)(fileSize); + bmpFileHeader[3] = (unsigned char)(fileSize >> 8); + bmpFileHeader[4] = (unsigned char)(fileSize >> 16); + bmpFileHeader[5] = (unsigned char)(fileSize >> 24); + + // Update the image width in the BMP info header + bmpInfoHeader[4] = (unsigned char)(width); + bmpInfoHeader[5] = (unsigned char)(width >> 8); + bmpInfoHeader[6] = (unsigned char)(width >> 16); + bmpInfoHeader[7] = (unsigned char)(width >> 24); + + // Update the image height in the BMP info header + bmpInfoHeader[8] = (unsigned char)(height); + bmpInfoHeader[9] = (unsigned char)(height >> 8); + bmpInfoHeader[10] = (unsigned char)(height >> 16); + bmpInfoHeader[11] = (unsigned char)(height >> 24); + + fwrite(bmpFileHeader, sizeof(unsigned char), 14, file); + fwrite(bmpInfoHeader, sizeof(unsigned char), 40, file); + + return paddingSize; +} -/** -* Writes BMP image to file. This function is used to write a bmp image to a file. -* -* @param filename - Name of file to write to. The file must end with. bmp -* @param img - Array of uint8_t that contains the image -*/ -void writeBMP(const char* filename, uint8_t img[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]) { - int width = APP_IMAGE_WIDTH_PIXELS; - int height = APP_IMAGE_HEIGHT_PIXELS; - int channels = APP_IMAGE_CHANNEL_COUNT; - // Define BMP file header and info header - unsigned char bmpFileHeader[14] = { - 'B', 'M', // Signature - 0, 0, 0, 0, // File size (to be filled later) - 0, 0, // Reserved - 0, 0, // Reserved - 54, 0, 0, 0 // Offset to image data - }; - - unsigned char bmpInfoHeader[40] = { - 40, 0, 0, 0, // Info header size - 0, 0, 0, 0, // Image width (to be filled later) - 0, 0, 0, 0, // Image height (to be filled later) - 1, 0, // Number of color planes - 8 * channels, 0, // Bits per pixel - 0, 0, 0, 0, // Compression method - 0, 0, 0, 0, // Image size (to be filled later) - 0, 0, 0, 0, // Horizontal resolution (pixel per meter) - 0, 0, 0, 0, // Vertical resolution (pixel per meter) - 0, 0, 0, 0, // Number of colors in the palette - 0, 0, 0, 0, // Number of important colors - }; - - // Calculate the row size (including padding) - int rowSize = width * channels; - int paddingSize = (4 - (rowSize % 4)) % 4; - int rowSizeWithPadding = rowSize + paddingSize; - - // Calculate the file size - int fileSize = 54 + (rowSizeWithPadding * height); - - // Update the file size in the BMP file header - bmpFileHeader[2] = (unsigned char)(fileSize); - bmpFileHeader[3] = (unsigned char)(fileSize >> 8); - bmpFileHeader[4] = (unsigned char)(fileSize >> 16); - bmpFileHeader[5] = (unsigned char)(fileSize >> 24); - - // Update the image width in the BMP info header - bmpInfoHeader[4] = (unsigned char)(width); - bmpInfoHeader[5] = (unsigned char)(width >> 8); - bmpInfoHeader[6] = (unsigned char)(width >> 16); - bmpInfoHeader[7] = (unsigned char)(width >> 24); - - // Update the image height in the BMP info header - bmpInfoHeader[8] = (unsigned char)(height); - bmpInfoHeader[9] = (unsigned char)(height >> 8); - bmpInfoHeader[10] = (unsigned char)(height >> 16); - bmpInfoHeader[11] = (unsigned char)(height >> 24); - - // Open the file for writing - FILE* file = fopen(filename, "wb"); - if (!file) { - printf("Error opening file for writing: %s\n", filename); - return; - } - // Write the BMP file header and info header - fwrite(bmpFileHeader, sizeof(unsigned char), 14, file); - fwrite(bmpInfoHeader, sizeof(unsigned char), 40, file); - - // Write the image data row by row (with padding) - int i, j; - for (i = height - 1; i >= 0; i--) { - for (j = 0; j < width; j++) { - // Write the pixel data (assuming RGB order) - fwrite(&img[2][i][j], sizeof(unsigned char), 1, file); // Blue - fwrite(&img[1][i][j], sizeof(unsigned char), 1, file); // Green - fwrite(&img[0][i][j], sizeof(unsigned char), 1, file); // Red - // For 4-channel images, you can write the alpha channel here - // fwrite(&img[index + 3], sizeof(unsigned char), 1, file); // Alpha - } - - // Write the padding bytes - for (j = 0; j < paddingSize; j++) { - fwrite("\0", sizeof(unsigned char), 1, file); - } - } - // Close the file - fclose(file); - printf("Outfile %s\n", filename); - printf("image size (%dx%d)\n", APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_HEIGHT_PIXELS); -} +void writeBMP( + const char* filename, + uint8_t img[], + const unsigned height, + const unsigned width) +{ + int8_t (*img3d)[height][width] = (int8_t (*)[height][width]) img; + // Open the file for writing + FILE* file = fopen(filename, "wb"); + if (!file) { + printf("Error opening file for writing: %s\n", filename); + return; + } -// Convert int8_t to uint8_t image -void img_int8_to_uint8( - int8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS], - uint8_t out_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS] -) -{ - // Add 128 to all elements - for(uint16_t c = 0; c < APP_IMAGE_CHANNEL_COUNT; c++){ - for(uint16_t k = 0; k < APP_IMAGE_HEIGHT_PIXELS; k++){ - for(uint16_t j = 0; j < APP_IMAGE_WIDTH_PIXELS; j++){ - int8_t val = image_buffer[c][k][j]; - out_buffer[c][k][j] = val + 128; - } + // Write the BMP file header and info header + int paddingSize = writeBMPHeader(file, height, width); + + // Write the image data row by row (with padding) + int i, j; + for (i = height - 1; i >= 0; i--) { + for (j = 0; j < width; j++) { + // Write the pixel data (assuming RGB order) + fwrite(&img3d[2][i][j], sizeof(unsigned char), 1, file); // Blue + fwrite(&img3d[1][i][j], sizeof(unsigned char), 1, file); // Green + fwrite(&img3d[0][i][j], sizeof(unsigned char), 1, file); // Red + // For 4-channel images, you can write the alpha channel here + // fwrite(&img[index + 3], sizeof(unsigned char), 1, file); // Alpha + } + + // Write the padding bytes + for (j = 0; j < paddingSize; j++) { + fwrite("\0", sizeof(unsigned char), 1, file); } } + + // Close the file + fclose(file); + printf("Outfile %s\n", filename); + printf("image size (%dx%d)\n", width, height); } -// Convert int8_t to uint8_t image -void img_int8_to_uint8_inplace(int8_t image_buffer[H_RAW][W_RAW]) +/** + * Convert an array of int8 to an array of uint8. + * + * Data can be updated in-place. + */ +void vect_int8_to_uint8( + uint8_t output[], + int8_t input[], + const unsigned length) { - // Add 128 to all elements - for (int i = 0; i < H_RAW; i++) { - for (int j = 0; j < W_RAW; j++) { - image_buffer[i][j] += 128; - } - } -} + for(int k = 0; k < length; k++) + output[k] = input[k] + 128; +} \ No newline at end of file diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index 44788824..33adf6a0 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -7,10 +7,9 @@ void user_app() { int8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; - uint8_t out_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; // set the input image to 0 - memset(image_buffer, -128, APP_IMAGE_CHANNEL_COUNT * APP_IMAGE_HEIGHT_PIXELS * APP_IMAGE_WIDTH_PIXELS); + memset(image_buffer, -128, sizeof(image_buffer)); // Wait for the image to set exposure delay_milliseconds(5000); @@ -25,13 +24,22 @@ void user_app() // rotate_image(image_buffer); // convert to uint8 - img_int8_to_uint8(image_buffer, out_buffer); + vect_int8_to_uint8((uint8_t*) image_buffer, + (int8_t*) image_buffer, + sizeof(image_buffer)); // Write binary file and .bmp file - write_image("capture.bin", out_buffer); + write_image("capture.bin", + (uint8_t*) image_buffer, + APP_IMAGE_CHANNEL_COUNT, + APP_IMAGE_HEIGHT_PIXELS, + APP_IMAGE_WIDTH_PIXELS); //save it to bmp - writeBMP("capture.bmp", out_buffer); + writeBMP("capture.bmp", + (uint8_t*) image_buffer, + APP_IMAGE_HEIGHT_PIXELS, + APP_IMAGE_WIDTH_PIXELS); // end here exit(0); diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index d362d7da..85236c6a 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -27,7 +27,9 @@ void user_app_raw(){ printf("Image captured...\n"); // Convert image from int8 to uint8 in-place - img_int8_to_uint8_inplace(image_buffer); + vect_int8_to_uint8((uint8_t*) image_buffer, + (int8_t*) image_buffer, + sizeof(image_buffer)); // Save the image to a file write_image_file("capture.bin", (uint8_t * ) &image_buffer[0][0], From 853cc3895cb9b3962fd9e359600be2ef5eb92308 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 13 Jun 2023 13:18:09 +0100 Subject: [PATCH 080/306] updating example readmes --- examples/take_picture_downsample/README.md | 13 ----- examples/take_picture_downsample/README.rst | 56 +++++++++++++++++++++ examples/take_picture_raw/README.rst | 9 ++-- 3 files changed, 61 insertions(+), 17 deletions(-) delete mode 100644 examples/take_picture_downsample/README.md create mode 100644 examples/take_picture_downsample/README.rst diff --git a/examples/take_picture_downsample/README.md b/examples/take_picture_downsample/README.md deleted file mode 100644 index cf6ae175..00000000 --- a/examples/take_picture_downsample/README.md +++ /dev/null @@ -1,13 +0,0 @@ -## Example: Take picture - -This example set the basic settings for the sony sensor and grab a single frame. -By default the format is the following: -* 640x480 RAW8 - -### File Description -* Sensor.h : configures and set the basic settings for the sensor -* Main.xc : starts 2 threads, i2C control and mipi capture -* Process_frame.c : write the image to a file - -## Build example -Run the following command: ```make example_take_picture``` \ No newline at end of file diff --git a/examples/take_picture_downsample/README.rst b/examples/take_picture_downsample/README.rst new file mode 100644 index 00000000..377a7896 --- /dev/null +++ b/examples/take_picture_downsample/README.rst @@ -0,0 +1,56 @@ +================================ +Example: Take picture downsample +================================ + +This example set the basic settings for the sony sensor and grab a single frame. +By default the format is the following: +- 640x480 RAW8 + +************* +Build example +************* +Run the following commands from the top level: + +.. tab:: Linux and Mac + + .. code-block:: console + + cmake -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -B build + make -C build example_take_picture_downsample + +.. tab:: Windows + + .. code-block:: console + + cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -B build + ninja -C build example_take_picture_downsample + +*************** +Running example +*************** + +From the top level + +.. tab:: Linux and Mac + + .. code-block:: console + + pip install -e utils/xscope_fileio + python python/run_xscope_bin.py build/examples/take_picture_downsample/example_take_picture_downsample.xe + +.. tab:: Windows + + .. code-block:: console + + # works with a cl compiler + pip install -e utils/xscope_fileio + cd utils/xscope_fileio/host + cmake -G "Ninja" . && ninja + cd ../../../ + python python/run_xscope_bin.py build/examples/take_picture_downsample/example_take_picture_downsample.xe + +****** +Output +****** + +The output files ``capture.bin`` and ``capture.bmp`` will be generated at the top level the repository. ``capture.bin`` can be further processed using ``python/decode_downsampled.py`` script. diff --git a/examples/take_picture_raw/README.rst b/examples/take_picture_raw/README.rst index 028031ca..f69a3efd 100644 --- a/examples/take_picture_raw/README.rst +++ b/examples/take_picture_raw/README.rst @@ -1,6 +1,6 @@ -===================== -Example: Take picture -===================== +========================= +Example: Take picture RAW +========================= This example set the basic settings for the sony sensor and grab a single frame. By default the format is the following: @@ -23,7 +23,7 @@ Run the following commands from the top level: .. code-block:: console cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -B build - ninja -C example_take_picture_raw + ninja -C build example_take_picture_raw *************** Running example @@ -42,6 +42,7 @@ From the top level .. code-block:: console + # works with a cl compiler pip install -e utils/xscope_fileio cd utils/xscope_fileio/host cmake -G "Ninja" . && ninja From 78cef3c0f6a25d6274416c23a8696758930bd601 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 13 Jun 2023 13:35:57 +0100 Subject: [PATCH 081/306] readme dimention fix --- examples/take_picture_downsample/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/take_picture_downsample/README.rst b/examples/take_picture_downsample/README.rst index 377a7896..a3c3a9db 100644 --- a/examples/take_picture_downsample/README.rst +++ b/examples/take_picture_downsample/README.rst @@ -4,7 +4,7 @@ Example: Take picture downsample This example set the basic settings for the sony sensor and grab a single frame. By default the format is the following: -- 640x480 RAW8 +- 160x120x3 RGB ************* Build example From 63f0363724b71fa3dd9f34b5980a3f6826847775 Mon Sep 17 00:00:00 2001 From: xalbertoisorna Date: Tue, 13 Jun 2023 14:28:15 +0100 Subject: [PATCH 082/306] auto white balancing and isp cleanup --- camera/api/isp.h | 9 +++--- camera/api/statistics.h | 7 +++++ camera/src/image_hfilter.c | 3 +- camera/src/isp.c | 57 ++++++++++++++++++++++++++---------- camera/src/statistics.c | 27 ++++++++++------- python/decode_downsampled.py | 2 +- python/utils.py | 6 ++-- sensors/api/sensor.h | 7 ----- 8 files changed, 76 insertions(+), 42 deletions(-) diff --git a/camera/api/isp.h b/camera/api/isp.h index 79a7a11c..4bc9e7bb 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -7,7 +7,8 @@ #include "statistics.h" - +// black level is sensor dependant (used by horizontal filter) +#define BLACK_LEVEL 16 // ---------------------------------- AE/AGC ------------------------------ void AE_control_exposure( @@ -21,9 +22,9 @@ uint8_t AE_compute_new_exposure(float exposure, float skewness); // ---------------------------------- AWB ------------------------------ // Initial channel scales -#define AWB_gain_RED 1.3 -#define AWB_gain_GREEN 0.8 -#define AWB_gain_BLUE 1.3 +#define AWB_gain_RED 1 +#define AWB_gain_GREEN 1 +#define AWB_gain_BLUE 1 /** * struct to hold the calculated parameters for the ISP diff --git a/camera/api/statistics.h b/camera/api/statistics.h index bfc787db..2c4b9ee7 100644 --- a/camera/api/statistics.h +++ b/camera/api/statistics.h @@ -25,6 +25,13 @@ extern "C" { #error HISTOGRAM_BIN_COUNT value not currently supported. #endif +// The percentile to look for when applying white balance adjustments, as a +// fraction. (0.95 will find the value which 95% of pixels are less than or +// equal to) +#ifndef APP_WB_PERCENTILE +#define APP_WB_PERCENTILE (0.94) +#endif + // Objects definitions typedef struct { int8_t pixels[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_WIDTH_PIXELS]; diff --git a/camera/src/image_hfilter.c b/camera/src/image_hfilter.c index 023434b6..e120fe46 100644 --- a/camera/src/image_hfilter.c +++ b/camera/src/image_hfilter.c @@ -40,6 +40,5 @@ void pixel_hfilter_update_scale( const float sum_b = b0 + 2*b1; - state->acc_init = (128 * (sum_b - shift_scale)); + state->acc_init = (128 * (sum_b - shift_scale - BLACK_LEVEL)); } - diff --git a/camera/src/isp.c b/camera/src/isp.c index 85247ea7..b75b2516 100644 --- a/camera/src/isp.c +++ b/camera/src/isp.c @@ -16,19 +16,23 @@ void AE_control_exposure( { // Initial exposure static uint8_t new_exp = AE_INITIAL_EXPOSURE; + static uint8_t printf_info = 1; + + // Compute skewness and adjust exposure if needed float sk = AE_compute_mean_skewness(global_stats); if (AE_is_adjusted(sk)){ - printf("-----> adjustement done\n"); + if (printf_info){ + printf("-----> adjustement done\n"); + printf_info = 0; + } } - else{ - // adjust exposure - new_exp = AE_compute_new_exposure((float) new_exp, sk); - // printf("new exp = %d\n", new_exp); - sensor_control_set_exposure(sc_if, (uint8_t) new_exp); + else{ // Adjust exposure + new_exp = AE_compute_new_exposure((float)new_exp, sk); + sensor_control_set_exposure(sc_if, (uint8_t)new_exp); + printf_info = 1; } } - void AE_print_skewness(global_stats_t *gstats){ printf("skewness:%f,%f,%f\n", (*gstats)[0].skewness, @@ -84,7 +88,7 @@ uint8_t AE_compute_new_exposure(float exposure, float skewness) // ---------------------------------- AWB ------------------------------ -const float AWB_ceil = 254.0; +const float AWB_ceil = 255.0; const float AWB_MAX = 1.6; const float AWB_MIN = 0.8; @@ -107,16 +111,26 @@ float AWB_clip_value(float tmp){ return tmp; } - -void AWB_compute_gains(global_stats_t *gstats, isp_params_t *isp_params){ +static +void AWB_compute_gains_percentile(global_stats_t *gstats, isp_params_t *isp_params){ // Adjust AWB - float tmp1, tmp2, tmp3; + float tmp1=1.3; + float tmp2=1.0; + float tmp3=1.3; + + // percentile adjustement + printf("%d,%d,%d,%d,%d,%d,\n", + (*gstats)[0].min, + (*gstats)[0].max, + (*gstats)[1].min, + (*gstats)[1].max, + (*gstats)[2].min, + (*gstats)[2].max); + + tmp1 = 254 / (float)(*gstats)[0].percentile; // RED + tmp2 = 254 / (float)(*gstats)[1].percentile; // GREEN + tmp3 = 254 / (float)(*gstats)[2].percentile; // BLUE - tmp1 = AWB_ceil / (float)(*gstats)[0].percentile; // RED - tmp2 = AWB_ceil / (float)(*gstats)[1].percentile; // GREEN - tmp3 = AWB_ceil / (float)(*gstats)[2].percentile; // BLUE - - // Proportional control with saturation to the white reference tmp1 = 0.2*(1.3-tmp1) + 1.3; tmp2 = 0.2*(1-tmp2) + 1; tmp3 = 0.2*(1.3-tmp3) + 1.3; @@ -130,6 +144,17 @@ void AWB_compute_gains(global_stats_t *gstats, isp_params_t *isp_params){ isp_params->channel_gain[2] = tmp3; } +void AWB_compute_gains(global_stats_t *gstats, isp_params_t *isp_params){ + // Adjust AWB + float tmp0=1.35; + float tmp1=1.0; + float tmp2=1.35; + + isp_params->channel_gain[0] = tmp0; + isp_params->channel_gain[1] = tmp1; + isp_params->channel_gain[2] = tmp2; +} + void AWB_print_gains(isp_params_t *isp_params){ printf("awb:%f,%f,%f\n", isp_params->channel_gain[0], diff --git a/camera/src/statistics.c b/camera/src/statistics.c index 4fdcc856..2735bea7 100644 --- a/camera/src/statistics.c +++ b/camera/src/statistics.c @@ -73,13 +73,25 @@ void compute_skewness(channel_stats_t *stats) void compute_simple_stats(channel_stats_t *stats) { // Calculate the histogram + uint8_t temp_min = 0; + uint8_t temp_max = 0; + for(int k = 0; k < HISTOGRAM_BIN_COUNT; k++){ unsigned bin = stats->histogram.bins[k]; + // mean stats->mean += bin * k; - stats->max = (stats->max >= bin)? stats->max : bin; - stats->min = (stats->min <= bin)? stats->min : bin; + // max and min + if (bin != 0){ + temp_max = k; + if (temp_min == 0){ + temp_min = k; + } + } + //stats->max = (stats->max >= bin)? stats->max : bin; + //stats->min = (stats->min <= bin)? stats->min : bin; } - + stats->max = temp_max; + stats->min = temp_min; // biased downwards due to truncation stats->max <<= APP_HISTOGRAM_QUANTIZATION_BITS; stats->min <<= APP_HISTOGRAM_QUANTIZATION_BITS; @@ -119,23 +131,18 @@ void statistics_thread( { // Outer loop iterates over frames while(1){ - // Declare new stats global_stats_t global_stats = {{0}}; // Inner loop iterates over rows within a frame while(1){ low_res_image_row_t* row = (low_res_image_row_t*) s_chan_in_word(c_img_in); - // Signal end of frame [1] - if(row == NULL) + if(row == NULL) // Signal end of frame [1] break; // Update histogram for(uint8_t channel = 0; channel < APP_IMAGE_CHANNEL_COUNT; channel++){ - update_histogram( - &global_stats[channel].histogram, - &row->pixels[channel][0] - ); + update_histogram(&global_stats[channel].histogram, &row->pixels[channel][0]); } } diff --git a/python/decode_downsampled.py b/python/decode_downsampled.py index 681b2203..951d74ad 100644 --- a/python/decode_downsampled.py +++ b/python/decode_downsampled.py @@ -31,4 +31,4 @@ plt.show() # show histograms -show_histogram_by_channel(img) +show_histogram_by_channel(img, 3000) diff --git a/python/utils.py b/python/utils.py index 1e897cd9..24e42d53 100644 --- a/python/utils.py +++ b/python/utils.py @@ -462,9 +462,9 @@ def run_histogram_equalization(img_bgr): return colorimage_clahe -def show_histogram_by_channel(image): +def show_histogram_by_channel(image, ylim=None): # Set the histogram bins to 256, the range to 0-255 - hist_size = 260 + hist_size = 265 hist_range = (0, 255) # Plot the histograms using plt.hist @@ -473,6 +473,8 @@ def show_histogram_by_channel(image): plt.subplot(1, 3, i+1) plt.title(f'{col.upper()} Histogram') plt.xlim([0, hist_size]) + if ylim is not None: + plt.ylim([0, ylim]) plt.hist(image[:,:,i].ravel(), bins=hist_size, range=hist_range, color=col) plt.show() diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index 2f0bd5ce..d85525f6 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -154,13 +154,6 @@ #define APP_HISTOGRAM_SAMPLE_STEP (1) #endif -// The percentile to look for when applying white balance adjustments, as a -// fraction. (0.95 will find the value which 95% of pixels are less than or -// equal to) -#ifndef APP_WB_PERCENTILE -#define APP_WB_PERCENTILE (0.95) -#endif - // For simplicity here #define CH (APP_IMAGE_CHANNEL_COUNT) #define H (APP_IMAGE_HEIGHT_PIXELS) From 90756aaf8ca9f12a1f3d50b1221e2beb5c5697a8 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 13 Jun 2023 16:19:41 +0100 Subject: [PATCH 083/306] fixing typo and the example --- examples/take_picture_downsample/src/app.c | 3 ++- utils/io_utils/io_utils.c | 2 +- utils/io_utils/io_utils.h | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index bfdd93af..2a51769b 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -31,9 +31,10 @@ void user_app() (int8_t*) image_buffer, sizeof(image_buffer)); + memcpy(temp_buffer, image_buffer, APP_IMAGE_CHANNEL_COUNT * APP_IMAGE_HEIGHT_PIXELS * APP_IMAGE_WIDTH_PIXELS * sizeof(uint8_t)); uint8_t * io_buff = (uint8_t *) &image_buffer[0][0][0]; // io_buff this will have [APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS][APP_IMAGE_CHANNEL_COUNT] dimentions - swap_dimentions((uint8_t *) &temp_buffer[0][0][0], io_buff, + swap_dimensions((uint8_t *) &temp_buffer[0][0][0], io_buff, APP_IMAGE_HEIGHT_PIXELS, APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_CHANNEL_COUNT); diff --git a/utils/io_utils/io_utils.c b/utils/io_utils/io_utils.c index 134bba15..f75ad126 100644 --- a/utils/io_utils/io_utils.c +++ b/utils/io_utils/io_utils.c @@ -2,7 +2,7 @@ #include "io_utils.h" #include -void swap_dimentions(uint8_t * image_in, uint8_t * image_out, const size_t height, const size_t width, const size_t channels) +void swap_dimensions(uint8_t * image_in, uint8_t * image_out, const size_t height, const size_t width, const size_t channels) { printf("Swapping image dimentions...\n"); for(size_t k = 0; k < height; k++) diff --git a/utils/io_utils/io_utils.h b/utils/io_utils/io_utils.h index f3a8d5d6..19e280a0 100644 --- a/utils/io_utils/io_utils.h +++ b/utils/io_utils/io_utils.h @@ -21,7 +21,7 @@ extern "C" { void write_file(char * filename, uint8_t * data, const size_t size); /** - * @brief Swaps image dimentions from [channel][height][width] + * @brief Swaps image dimensions from [channel][height][width] * to [height][width][channel] * * @param image_in Input image @@ -30,7 +30,7 @@ void write_file(char * filename, uint8_t * data, const size_t size); * @param width Image width * @param channels Number of channels */ -void swap_dimentions(uint8_t * image_in, uint8_t * image_out, const size_t height, const size_t width, const size_t channels); +void swap_dimensions(uint8_t * image_in, uint8_t * image_out, const size_t height, const size_t width, const size_t channels); /** * @brief Writes binary image file From e0c74d6cbe34bd93ada639c0dac51485381556ff Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 13 Jun 2023 17:00:28 +0100 Subject: [PATCH 084/306] fixing color indexing issue --- utils/io_utils/io_utils.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/utils/io_utils/io_utils.c b/utils/io_utils/io_utils.c index f75ad126..6bd4915a 100644 --- a/utils/io_utils/io_utils.c +++ b/utils/io_utils/io_utils.c @@ -4,15 +4,15 @@ void swap_dimensions(uint8_t * image_in, uint8_t * image_out, const size_t height, const size_t width, const size_t channels) { - printf("Swapping image dimentions...\n"); + printf("Swapping image dimensions...\n"); for(size_t k = 0; k < height; k++) { for(size_t j = 0; j < width; j++) { for(size_t c = 0; c < channels; c++) { - size_t index_in = c * (height * width) + k * width + j - 1; - size_t index_out = k * (width * channels) + j * channels + c - 1; + size_t index_in = c * (height * width) + k * width + j; + size_t index_out = k * (width * channels) + j * channels + c; image_out[index_out] = image_in[index_in]; } } @@ -104,7 +104,7 @@ void write_bmp_file(char * filename, uint8_t * image, const size_t height, const for(size_t j = 0; j < width; j++) { // Write the pixel data (assuming RGB order) - size_t offset = i * (channels * width) + j * channels - 1; + size_t offset = i * (channels * width) + j * channels; xscope_fwrite(&fp, &image[offset + 2], 1 * sizeof(uint8_t)); // Blue xscope_fwrite(&fp, &image[offset + 1], 1 * sizeof(uint8_t)); // Green xscope_fwrite(&fp, &image[offset + 0], 1 * sizeof(uint8_t)); // Red From dbd698d89802ffb2acbf3791da47145cd0c74cbc Mon Sep 17 00:00:00 2001 From: Alberto Gonzalez Isorna Date: Wed, 14 Jun 2023 10:54:52 +0100 Subject: [PATCH 085/306] sensors restructure --- README.md | 8 + sensors/CMakeLists.txt | 6 +- sensors/{api => _api}/sensor.h | 330 ++-- sensors/{api => _api}/sensor_control.h | 0 sensors/{api => _api}/sensor_defs.h | 0 sensors/{ => _src}/sensor_control.xc | 0 sensors/{ => galaxycore_gc2145}/gc2145.h | 70 +- sensors/{ => galaxycore_gc2145}/gc2145.xc | 320 +-- .../{ => galaxycore_gc2145}/gc2145_setup.xc | 1712 ++++++++--------- sensors/galaxycore_gc2145/readme.rst | 25 + sensors/{ => sony_imx219}/imx219.h | 146 +- sensors/{ => sony_imx219}/imx219.xc | 364 ++-- sensors/{ => sony_imx219}/imx219_reg.h | 652 +++---- sensors/sony_imx219/readme.rst | 25 + 14 files changed, 1859 insertions(+), 1799 deletions(-) rename sensors/{api => _api}/sensor.h (96%) rename sensors/{api => _api}/sensor_control.h (100%) rename sensors/{api => _api}/sensor_defs.h (100%) rename sensors/{ => _src}/sensor_control.xc (100%) rename sensors/{ => galaxycore_gc2145}/gc2145.h (95%) rename sensors/{ => galaxycore_gc2145}/gc2145.xc (96%) rename sensors/{ => galaxycore_gc2145}/gc2145_setup.xc (96%) create mode 100644 sensors/galaxycore_gc2145/readme.rst rename sensors/{ => sony_imx219}/imx219.h (96%) rename sensors/{ => sony_imx219}/imx219.xc (96%) rename sensors/{ => sony_imx219}/imx219_reg.h (95%) create mode 100644 sensors/sony_imx219/readme.rst diff --git a/README.md b/README.md index 534880a4..527d491d 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ This repository serves as a comprehensive software solution for camera manipulat - CMAKE - XMOS tools - git submodules +- Ninja (Windows) ## Installation Some dependent components are included as git submodules. These can be obtained by cloning this repository with the following command: @@ -21,11 +22,18 @@ git clone --recurse-submodules git@github.com:xmos/fwk_camera.git ``` ## Build +Linux, Mac ``` sh launch_cmake.sh cd build/ make {YOUR_EXAMPLE} ``` +Windows +``` +cmake -G Ninja -B build -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake +cd build/ +ninja +``` ## Useful commands - run (explorer board): ```xrun --xscope example_take_picture.xe``` diff --git a/sensors/CMakeLists.txt b/sensors/CMakeLists.txt index 76eced6c..ed3d0322 100644 --- a/sensors/CMakeLists.txt +++ b/sensors/CMakeLists.txt @@ -19,8 +19,10 @@ add_library(${LIB_NAME} STATIC) target_include_directories(${LIB_NAME} PUBLIC - . - api + _api + _src + galaxycore_gc2145 + sony_imx219 ) target_sources(${LIB_NAME} diff --git a/sensors/api/sensor.h b/sensors/_api/sensor.h similarity index 96% rename from sensors/api/sensor.h rename to sensors/_api/sensor.h index d85525f6..62c4a724 100644 --- a/sensors/api/sensor.h +++ b/sensors/_api/sensor.h @@ -1,165 +1,165 @@ -// Sensor.h settings needed for custom sensor configuration -#ifndef SENSOR_H -#define SENSOR_H - -#define XSTR(x) STR(x) -#define STR(x) #x - -// -------------- Sensor abstraction layer. -------------- -#include "sensor_defs.h" - -// Camera support -#define CONFIG_IMX219_SUPPORT ENABLED -#define CONFIG_GC2145_SUPPORT DISABLED - -// Crop selection -#define CROP_ENABLED DISABLED -#define CONFIG_MODE MODE_VGA_640x480 - -// Mipi format and mode -#define CONFIG_MIPI_FORMAT MIPI_DT_RAW8 -#define MIPI_PKT_BUFFER_COUNT 4 - -// FPS settings -#define FPS_13 // allowed values: [FPS_13, FPS_24, FPS_30, FPS_53, FPS_76] - -// -------------------------------------------------------- - -// Include custom libraries -#if CONFIG_IMX219_SUPPORT - #include "imx219.h" -#endif - -#if CONFIG_GC2145_SUPPORT - #include "gc2145.h" -#endif - - -// Modes configurations -#ifndef CONFIG_MODE - #error CONFIG_MODE has to be defined -#endif - -#if (CONFIG_MODE == MODE_VGA_640x480) - #define MIPI_IMAGE_WIDTH_PIXELS 640 // csi2 packed (stride 800) - #define MIPI_IMAGE_HEIGHT_PIXELS 480 - -#elif (CONFIG_MODE == MODE_UXGA_1640x1232) - #define MIPI_IMAGE_WIDTH_PIXELS 1640 // csi2 packed (stride 800) - #define MIPI_IMAGE_HEIGHT_PIXELS 1232 - -#elif (CONFIG_MODE == MODE_WQSXGA_3280x2464) - #define MIPI_IMAGE_WIDTH_PIXELS 3280 // csi2 packed (stride 800) - #define MIPI_IMAGE_HEIGHT_PIXELS 2464 - -#elif (CONFIG_MODE == MODE_FHD_1920x1080) - #define MIPI_IMAGE_WIDTH_PIXELS 1920 // csi2 packed (stride 800) - #define MIPI_IMAGE_HEIGHT_PIXELS 1080 - -#else - #error Unknown configuration mode -#endif - -// Pixel format configurations -#ifndef CONFIG_MIPI_FORMAT - #error CONFIG_MIPI_FORMAT has to be specified -#else - #if (CONFIG_MIPI_FORMAT == MIPI_DT_RAW10) - #define MIPI_IMAGE_WIDTH_BYTES (((MIPI_IMAGE_WIDTH_PIXELS) >> 2) * 5) // by 5/4 - - #elif (CONFIG_MIPI_FORMAT == MIPI_DT_RAW8) - #define MIPI_IMAGE_WIDTH_BYTES MIPI_IMAGE_WIDTH_PIXELS // same size - - #else - #error CONFIG_MIPI_FORMAT not supported - #endif -#endif - - -// Cropping configurations -#if (CROP_ENABLED) - #define CROP_WIDTH_PIXELS 320 - #define CROP_HEIGHT_PIXELS 240 - #define X_START_CROP 0 - #define Y_START_CROP 0 - // crop checks - #if (X_START_CROP + CROP_WIDTH_PIXELS) > MIPI_IMAGE_WIDTH_PIXELS - #error Crop X dimensions must be inside bounds - #endif - // crop checks - #if (Y_START_CROP + CROP_HEIGHT_PIXELS) > MIPI_IMAGE_HEIGHT_PIXELS - #error Crop Y dimensions must be inside bounds - #endif - -#endif - - -// ----------------------- Settings dependant of each sensor library - -// Camera dependant (do not edit) -#define MIPI_LINE_WIDTH_BYTES MIPI_IMAGE_WIDTH_BYTES -#define MIPI_MAX_PKT_SIZE_BYTES ((MIPI_LINE_WIDTH_BYTES) + 4) -#define MIPI_TILE 1 -#define EXPECTED_FORMAT CONFIG_MIPI_FORMAT //backward compatibility -#define MIPI_EXPECTED_FORMAT CONFIG_MIPI_FORMAT //backward compatibility -// SRAM Image storage (do not edit) -//TODO check maximum storage size for the image -#define MAX_MEMORY_SIZE 500000 << 2 //becasue half needed is code - -#if MIPI_IMAGE_WIDTH_BYTES*MIPI_IMAGE_HEIGHT_PIXELS > MAX_MEMORY_SIZE - #warning "The image appears to be too large for the available internal RAM.!" -#endif - - - - -// ---------------------------------------------------------------- -// ---------------------------------------------------------------- - -#define SENSOR_BIT_DEPTH (8) -#define SENSOR_RAW_IMAGE_WIDTH_PIXELS MIPI_IMAGE_WIDTH_PIXELS -#define SENSOR_RAW_IMAGE_HEIGHT_PIXELS MIPI_IMAGE_HEIGHT_PIXELS - -#define APP_DECIMATION_FACTOR (4) - -#define APP_IMAGE_WIDTH_PIXELS (SENSOR_RAW_IMAGE_WIDTH_PIXELS \ - / APP_DECIMATION_FACTOR) - -#define APP_IMAGE_HEIGHT_PIXELS (SENSOR_RAW_IMAGE_HEIGHT_PIXELS \ - / APP_DECIMATION_FACTOR) - -#define APP_IMAGE_CHANNEL_COUNT (3) - -#define APP_IMAGE_SIZE_PIXELS (APP_IMAGE_WIDTH_PIXELS \ - * APP_IMAGE_HEIGHT_PIXELS) - -#define APP_IMAGE_SIZE_BYTES (APP_IMAGE_SIZE_PIXELS \ - * APP_IMAGE_CHANNEL_COUNT ) - - -#define CHAN_RED 0 -#define CHAN_GREEN 1 -#define CHAN_BLUE 2 - -// Number of bits to collapse channel cardinality (larger value results in fewer -// histogram bins) -#ifndef APP_HISTOGRAM_QUANTIZATION_BITS -#define APP_HISTOGRAM_QUANTIZATION_BITS (2) -#endif - - -// Not every pixel of the image will be sampled. This is the distance between -// sampled values in a row. -#ifndef APP_HISTOGRAM_SAMPLE_STEP -#define APP_HISTOGRAM_SAMPLE_STEP (1) -#endif - -// For simplicity here -#define CH (APP_IMAGE_CHANNEL_COUNT) -#define H (APP_IMAGE_HEIGHT_PIXELS) -#define W (APP_IMAGE_WIDTH_PIXELS) - -#define H_RAW (MIPI_IMAGE_HEIGHT_PIXELS) -#define W_RAW (MIPI_IMAGE_WIDTH_PIXELS) - -#endif // sensor_H +// Sensor.h settings needed for custom sensor configuration +#ifndef SENSOR_H +#define SENSOR_H + +#define XSTR(x) STR(x) +#define STR(x) #x + +// -------------- Sensor abstraction layer. -------------- +#include "sensor_defs.h" + +// Camera support +#define CONFIG_IMX219_SUPPORT ENABLED +#define CONFIG_GC2145_SUPPORT DISABLED + +// Crop selection +#define CROP_ENABLED DISABLED +#define CONFIG_MODE MODE_VGA_640x480 + +// Mipi format and mode +#define CONFIG_MIPI_FORMAT MIPI_DT_RAW8 +#define MIPI_PKT_BUFFER_COUNT 4 + +// FPS settings +#define FPS_13 // allowed values: [FPS_13, FPS_24, FPS_30, FPS_53, FPS_76] + +// -------------------------------------------------------- + +// Include custom libraries +#if CONFIG_IMX219_SUPPORT + #include "imx219.h" +#endif + +#if CONFIG_GC2145_SUPPORT + #include "gc2145.h" +#endif + + +// Modes configurations +#ifndef CONFIG_MODE + #error CONFIG_MODE has to be defined +#endif + +#if (CONFIG_MODE == MODE_VGA_640x480) + #define MIPI_IMAGE_WIDTH_PIXELS 640 // csi2 packed (stride 800) + #define MIPI_IMAGE_HEIGHT_PIXELS 480 + +#elif (CONFIG_MODE == MODE_UXGA_1640x1232) + #define MIPI_IMAGE_WIDTH_PIXELS 1640 // csi2 packed (stride 800) + #define MIPI_IMAGE_HEIGHT_PIXELS 1232 + +#elif (CONFIG_MODE == MODE_WQSXGA_3280x2464) + #define MIPI_IMAGE_WIDTH_PIXELS 3280 // csi2 packed (stride 800) + #define MIPI_IMAGE_HEIGHT_PIXELS 2464 + +#elif (CONFIG_MODE == MODE_FHD_1920x1080) + #define MIPI_IMAGE_WIDTH_PIXELS 1920 // csi2 packed (stride 800) + #define MIPI_IMAGE_HEIGHT_PIXELS 1080 + +#else + #error Unknown configuration mode +#endif + +// Pixel format configurations +#ifndef CONFIG_MIPI_FORMAT + #error CONFIG_MIPI_FORMAT has to be specified +#else + #if (CONFIG_MIPI_FORMAT == MIPI_DT_RAW10) + #define MIPI_IMAGE_WIDTH_BYTES (((MIPI_IMAGE_WIDTH_PIXELS) >> 2) * 5) // by 5/4 + + #elif (CONFIG_MIPI_FORMAT == MIPI_DT_RAW8) + #define MIPI_IMAGE_WIDTH_BYTES MIPI_IMAGE_WIDTH_PIXELS // same size + + #else + #error CONFIG_MIPI_FORMAT not supported + #endif +#endif + + +// Cropping configurations +#if (CROP_ENABLED) + #define CROP_WIDTH_PIXELS 320 + #define CROP_HEIGHT_PIXELS 240 + #define X_START_CROP 0 + #define Y_START_CROP 0 + // crop checks + #if (X_START_CROP + CROP_WIDTH_PIXELS) > MIPI_IMAGE_WIDTH_PIXELS + #error Crop X dimensions must be inside bounds + #endif + // crop checks + #if (Y_START_CROP + CROP_HEIGHT_PIXELS) > MIPI_IMAGE_HEIGHT_PIXELS + #error Crop Y dimensions must be inside bounds + #endif + +#endif + + +// ----------------------- Settings dependant of each sensor library + +// Camera dependant (do not edit) +#define MIPI_LINE_WIDTH_BYTES MIPI_IMAGE_WIDTH_BYTES +#define MIPI_MAX_PKT_SIZE_BYTES ((MIPI_LINE_WIDTH_BYTES) + 4) +#define MIPI_TILE 1 +#define EXPECTED_FORMAT CONFIG_MIPI_FORMAT //backward compatibility +#define MIPI_EXPECTED_FORMAT CONFIG_MIPI_FORMAT //backward compatibility +// SRAM Image storage (do not edit) +//TODO check maximum storage size for the image +#define MAX_MEMORY_SIZE 500000 << 2 //becasue half needed is code + +#if MIPI_IMAGE_WIDTH_BYTES*MIPI_IMAGE_HEIGHT_PIXELS > MAX_MEMORY_SIZE + #warning "The image appears to be too large for the available internal RAM.!" +#endif + + + + +// ---------------------------------------------------------------- +// ---------------------------------------------------------------- + +#define SENSOR_BIT_DEPTH (8) +#define SENSOR_RAW_IMAGE_WIDTH_PIXELS MIPI_IMAGE_WIDTH_PIXELS +#define SENSOR_RAW_IMAGE_HEIGHT_PIXELS MIPI_IMAGE_HEIGHT_PIXELS + +#define APP_DECIMATION_FACTOR (4) + +#define APP_IMAGE_WIDTH_PIXELS (SENSOR_RAW_IMAGE_WIDTH_PIXELS \ + / APP_DECIMATION_FACTOR) + +#define APP_IMAGE_HEIGHT_PIXELS (SENSOR_RAW_IMAGE_HEIGHT_PIXELS \ + / APP_DECIMATION_FACTOR) + +#define APP_IMAGE_CHANNEL_COUNT (3) + +#define APP_IMAGE_SIZE_PIXELS (APP_IMAGE_WIDTH_PIXELS \ + * APP_IMAGE_HEIGHT_PIXELS) + +#define APP_IMAGE_SIZE_BYTES (APP_IMAGE_SIZE_PIXELS \ + * APP_IMAGE_CHANNEL_COUNT ) + + +#define CHAN_RED 0 +#define CHAN_GREEN 1 +#define CHAN_BLUE 2 + +// Number of bits to collapse channel cardinality (larger value results in fewer +// histogram bins) +#ifndef APP_HISTOGRAM_QUANTIZATION_BITS +#define APP_HISTOGRAM_QUANTIZATION_BITS (2) +#endif + + +// Not every pixel of the image will be sampled. This is the distance between +// sampled values in a row. +#ifndef APP_HISTOGRAM_SAMPLE_STEP +#define APP_HISTOGRAM_SAMPLE_STEP (1) +#endif + +// For simplicity here +#define CH (APP_IMAGE_CHANNEL_COUNT) +#define H (APP_IMAGE_HEIGHT_PIXELS) +#define W (APP_IMAGE_WIDTH_PIXELS) + +#define H_RAW (MIPI_IMAGE_HEIGHT_PIXELS) +#define W_RAW (MIPI_IMAGE_WIDTH_PIXELS) + +#endif // sensor_H diff --git a/sensors/api/sensor_control.h b/sensors/_api/sensor_control.h similarity index 100% rename from sensors/api/sensor_control.h rename to sensors/_api/sensor_control.h diff --git a/sensors/api/sensor_defs.h b/sensors/_api/sensor_defs.h similarity index 100% rename from sensors/api/sensor_defs.h rename to sensors/_api/sensor_defs.h diff --git a/sensors/sensor_control.xc b/sensors/_src/sensor_control.xc similarity index 100% rename from sensors/sensor_control.xc rename to sensors/_src/sensor_control.xc diff --git a/sensors/gc2145.h b/sensors/galaxycore_gc2145/gc2145.h similarity index 95% rename from sensors/gc2145.h rename to sensors/galaxycore_gc2145/gc2145.h index 9dc53ffe..7692427f 100644 --- a/sensors/gc2145.h +++ b/sensors/galaxycore_gc2145/gc2145.h @@ -1,36 +1,36 @@ -#include -#include "i2c.h" - -#define GAIN_MIN_DB 0 -#define GAIN_MAX_DB 84 -#define GAIN_DEFAULT_DB 50 - -#define GC2145_I2C_ADDR (0x3C) - -#define GC2145MIPI_2Lane -#define GC2145MIPI_TEST_PATTERN_CHECKSUM 0x54ddf19b - -/* GC2145 supported geometry */ -#define GC2145_WIDTH (1600) -#define GC2145_HEIGHT (1200) - -int gc2145_stream_start(client interface i2c_master_if i2c); -int gc2145_stream_stop(client interface i2c_master_if i2c); -int gc2145_init(client interface i2c_master_if i2c); -int gc2145_set_gain_dB(client interface i2c_master_if i2c, - uint32_t dBGain); - - -typedef struct { - uint16_t addr; - uint16_t val; -} gc_settings_t; - -extern const -gc_settings_t chip_set_up[]; - -extern const -size_t chip_set_up_length; - -#define SLEEP 0xFFFF +#include +#include "i2c.h" + +#define GAIN_MIN_DB 0 +#define GAIN_MAX_DB 84 +#define GAIN_DEFAULT_DB 50 + +#define GC2145_I2C_ADDR (0x3C) + +#define GC2145MIPI_2Lane +#define GC2145MIPI_TEST_PATTERN_CHECKSUM 0x54ddf19b + +/* GC2145 supported geometry */ +#define GC2145_WIDTH (1600) +#define GC2145_HEIGHT (1200) + +int gc2145_stream_start(client interface i2c_master_if i2c); +int gc2145_stream_stop(client interface i2c_master_if i2c); +int gc2145_init(client interface i2c_master_if i2c); +int gc2145_set_gain_dB(client interface i2c_master_if i2c, + uint32_t dBGain); + + +typedef struct { + uint16_t addr; + uint16_t val; +} gc_settings_t; + +extern const +gc_settings_t chip_set_up[]; + +extern const +size_t chip_set_up_length; + +#define SLEEP 0xFFFF #define TRSTUS 200 \ No newline at end of file diff --git a/sensors/gc2145.xc b/sensors/galaxycore_gc2145/gc2145.xc similarity index 96% rename from sensors/gc2145.xc rename to sensors/galaxycore_gc2145/gc2145.xc index 12b2e3a6..1103923c 100644 --- a/sensors/gc2145.xc +++ b/sensors/galaxycore_gc2145/gc2145.xc @@ -1,160 +1,160 @@ -#include -#include -#include -#include "i2c.h" -#include "gc2145.h" - -//#define SWIDTH 3296 -//#define SHEIGHT 2480 - -#define GC2145_I2C_ADDR (0x3C) - -#define GC2145MIPI_2Lane -#define GC2145MIPI_TEST_PATTERN_CHECKSUM 0x54ddf19b - -void exit(int); - -static -int i2c_write( - client interface i2c_master_if i2c, - int reg, - int value) -{ - i2c_regop_res_t result, res2; - result = i2c.write_reg(GC2145_I2C_ADDR, reg, value); - if (result != I2C_REGOP_SUCCESS) { - printf("Failed on address %02x value %02x\n", reg, value); - uint8_t val2; - val2 = i2c.read_reg(GC2145_I2C_ADDR, reg, res2); - printf("%02x %02x %02x %02x %d %d\n", GC2145_I2C_ADDR, reg, value, val2, res2, result); - } - return result != I2C_REGOP_SUCCESS ? (reg == 0xfe ? 0 : -1) : 0; -} - -static -int i2c_write_table( - client interface i2c_master_if i2c, - const gc_settings_t table[], - int N) -{ - int ret; - for(int i = 0; i < N; i++) { - uint32_t address = table[i].addr; - uint32_t value = table[i].val; - if (address == SLEEP) { - timer tmr; - int t; - tmr :> t; - tmr when timerafter(t + TRSTUS * 100) :> void; - } - if (address & 0x8000) { - address &= 0x7fff; - ret = i2c_write(i2c, address, value >> 8); - ret |= i2c_write(i2c, address+1, value & 0xff); - } else { - ret = i2c_write(i2c, address, value); - } - if (ret < 0) { - return ret; - } - } - for(int i = 240; i < 256; i++) { - } - return 0; -} - -static -uint8_t i2c_read( - client interface i2c_master_if i2c, - unsigned page, - uint8_t reg_addr) -{ - uint8_t val; - i2c_regop_res_t res; - res = i2c.write_reg(GC2145_I2C_ADDR, 0xFE, (page & 0x03)); - if(res) { printf("Register write error. Reg: 0xFE; Page: %u\n", page & 0x03); exit(1); } - val = i2c.read_reg(GC2145_I2C_ADDR, reg_addr, res); - if(res) { printf("Register read error. Reg: 0x%02X; Page: %u\n", (unsigned) reg_addr, page & 0x03); exit(1); } - res = i2c.write_reg(GC2145_I2C_ADDR, 0xFE, 0); - if(res) { printf("Register write error. Reg: 0xFE; Page: 0\n"); exit(1); } - return val; -} - -void gc2145_print_info( - client interface i2c_master_if i2c) -{ - i2c_regop_res_t result; - uint8_t value; - unsigned rewq; - -#define PRINT_REG(PAGE, REG_ADDR, TEXT) do { \ - value = i2c_read(i2c, PAGE, REG_ADDR); \ - printf("%s: 0x%02X (%u)\n", TEXT, (unsigned) value, (unsigned) value); \ - } while(0) - - #define PRINT_WIDE_REG(PAGE, REG_ADDR, TEXT) do { \ - value = i2c_read(i2c, PAGE, REG_ADDR); \ - rewq = ((unsigned)value) << 8; \ - value = i2c_read(i2c, PAGE, REG_ADDR+1); \ - rewq = (rewq | value); \ - printf("%s: 0x%04X (%u)\n", TEXT, rewq, rewq); \ - } while(0) - - PRINT_WIDE_REG(0, 0xF0, "Chip ID"); - PRINT_REG(0, 0xFB, "I2C Address"); - PRINT_REG(0, 0xF2, "Pad IO"); - PRINT_WIDE_REG(0, 0x03, "Exposure"); - PRINT_WIDE_REG(0, 0x05, "H Blanking"); - PRINT_WIDE_REG(0, 0x07, "V Blanking"); - PRINT_WIDE_REG(0, 0x0D, "Window Height"); - PRINT_WIDE_REG(0, 0x0F, "Window Width");; - PRINT_REG(0, 0x83, "Special effect"); - PRINT_REG(0, 0x84, "Output Format"); - PRINT_REG(0, 0x85, "Frame start num"); - PRINT_REG(0, 0x90, "Crop enable"); - PRINT_WIDE_REG(0, 0x91, "out_win.y[1]"); - PRINT_WIDE_REG(0, 0x93, "out_win.x[1]"); - PRINT_WIDE_REG(0, 0x95, "out_win.height"); - PRINT_WIDE_REG(0, 0x97, "out_win.width"); - -#undef PRINT_REG -} - - -int gc2145_stream_stop(client interface i2c_master_if i2c) { - int ret; - ret = i2c_write(i2c, 0xFE, 0x00); - ret = ret | i2c_write(i2c, 0xF2, 0x00); - return ret; -} - -int gc2145_stream_start(client interface i2c_master_if i2c) { - int ret; - ret = i2c_write(i2c, 0xFE, 0x00); - ret = ret | i2c_write(i2c, 0xF2, 0x0F); - return ret; -} - -int gc2145_init( - client interface i2c_master_if i2c) -{ - int ret; - printf("gc2145_stream_init()...\n"); - // gc2145_print_info(i2c); - - - ret = i2c_write_table(i2c, chip_set_up, chip_set_up_length); - if(ret) - printf("Failed to set all GC2145 registers.\n"); - - i2c_write(i2c, 0x84, 0x6); - - // i2c_write(i2c, 0xFE, 0xC0); - - gc2145_stream_stop(i2c); - - gc2145_print_info(i2c); - - return ret; -} - +#include +#include +#include +#include "i2c.h" +#include "gc2145.h" + +//#define SWIDTH 3296 +//#define SHEIGHT 2480 + +#define GC2145_I2C_ADDR (0x3C) + +#define GC2145MIPI_2Lane +#define GC2145MIPI_TEST_PATTERN_CHECKSUM 0x54ddf19b + +void exit(int); + +static +int i2c_write( + client interface i2c_master_if i2c, + int reg, + int value) +{ + i2c_regop_res_t result, res2; + result = i2c.write_reg(GC2145_I2C_ADDR, reg, value); + if (result != I2C_REGOP_SUCCESS) { + printf("Failed on address %02x value %02x\n", reg, value); + uint8_t val2; + val2 = i2c.read_reg(GC2145_I2C_ADDR, reg, res2); + printf("%02x %02x %02x %02x %d %d\n", GC2145_I2C_ADDR, reg, value, val2, res2, result); + } + return result != I2C_REGOP_SUCCESS ? (reg == 0xfe ? 0 : -1) : 0; +} + +static +int i2c_write_table( + client interface i2c_master_if i2c, + const gc_settings_t table[], + int N) +{ + int ret; + for(int i = 0; i < N; i++) { + uint32_t address = table[i].addr; + uint32_t value = table[i].val; + if (address == SLEEP) { + timer tmr; + int t; + tmr :> t; + tmr when timerafter(t + TRSTUS * 100) :> void; + } + if (address & 0x8000) { + address &= 0x7fff; + ret = i2c_write(i2c, address, value >> 8); + ret |= i2c_write(i2c, address+1, value & 0xff); + } else { + ret = i2c_write(i2c, address, value); + } + if (ret < 0) { + return ret; + } + } + for(int i = 240; i < 256; i++) { + } + return 0; +} + +static +uint8_t i2c_read( + client interface i2c_master_if i2c, + unsigned page, + uint8_t reg_addr) +{ + uint8_t val; + i2c_regop_res_t res; + res = i2c.write_reg(GC2145_I2C_ADDR, 0xFE, (page & 0x03)); + if(res) { printf("Register write error. Reg: 0xFE; Page: %u\n", page & 0x03); exit(1); } + val = i2c.read_reg(GC2145_I2C_ADDR, reg_addr, res); + if(res) { printf("Register read error. Reg: 0x%02X; Page: %u\n", (unsigned) reg_addr, page & 0x03); exit(1); } + res = i2c.write_reg(GC2145_I2C_ADDR, 0xFE, 0); + if(res) { printf("Register write error. Reg: 0xFE; Page: 0\n"); exit(1); } + return val; +} + +void gc2145_print_info( + client interface i2c_master_if i2c) +{ + i2c_regop_res_t result; + uint8_t value; + unsigned rewq; + +#define PRINT_REG(PAGE, REG_ADDR, TEXT) do { \ + value = i2c_read(i2c, PAGE, REG_ADDR); \ + printf("%s: 0x%02X (%u)\n", TEXT, (unsigned) value, (unsigned) value); \ + } while(0) + + #define PRINT_WIDE_REG(PAGE, REG_ADDR, TEXT) do { \ + value = i2c_read(i2c, PAGE, REG_ADDR); \ + rewq = ((unsigned)value) << 8; \ + value = i2c_read(i2c, PAGE, REG_ADDR+1); \ + rewq = (rewq | value); \ + printf("%s: 0x%04X (%u)\n", TEXT, rewq, rewq); \ + } while(0) + + PRINT_WIDE_REG(0, 0xF0, "Chip ID"); + PRINT_REG(0, 0xFB, "I2C Address"); + PRINT_REG(0, 0xF2, "Pad IO"); + PRINT_WIDE_REG(0, 0x03, "Exposure"); + PRINT_WIDE_REG(0, 0x05, "H Blanking"); + PRINT_WIDE_REG(0, 0x07, "V Blanking"); + PRINT_WIDE_REG(0, 0x0D, "Window Height"); + PRINT_WIDE_REG(0, 0x0F, "Window Width");; + PRINT_REG(0, 0x83, "Special effect"); + PRINT_REG(0, 0x84, "Output Format"); + PRINT_REG(0, 0x85, "Frame start num"); + PRINT_REG(0, 0x90, "Crop enable"); + PRINT_WIDE_REG(0, 0x91, "out_win.y[1]"); + PRINT_WIDE_REG(0, 0x93, "out_win.x[1]"); + PRINT_WIDE_REG(0, 0x95, "out_win.height"); + PRINT_WIDE_REG(0, 0x97, "out_win.width"); + +#undef PRINT_REG +} + + +int gc2145_stream_stop(client interface i2c_master_if i2c) { + int ret; + ret = i2c_write(i2c, 0xFE, 0x00); + ret = ret | i2c_write(i2c, 0xF2, 0x00); + return ret; +} + +int gc2145_stream_start(client interface i2c_master_if i2c) { + int ret; + ret = i2c_write(i2c, 0xFE, 0x00); + ret = ret | i2c_write(i2c, 0xF2, 0x0F); + return ret; +} + +int gc2145_init( + client interface i2c_master_if i2c) +{ + int ret; + printf("gc2145_stream_init()...\n"); + // gc2145_print_info(i2c); + + + ret = i2c_write_table(i2c, chip_set_up, chip_set_up_length); + if(ret) + printf("Failed to set all GC2145 registers.\n"); + + i2c_write(i2c, 0x84, 0x6); + + // i2c_write(i2c, 0xFE, 0xC0); + + gc2145_stream_stop(i2c); + + gc2145_print_info(i2c); + + return ret; +} + diff --git a/sensors/gc2145_setup.xc b/sensors/galaxycore_gc2145/gc2145_setup.xc similarity index 96% rename from sensors/gc2145_setup.xc rename to sensors/galaxycore_gc2145/gc2145_setup.xc index 2fd29b77..9ad5e73f 100644 --- a/sensors/gc2145_setup.xc +++ b/sensors/galaxycore_gc2145/gc2145_setup.xc @@ -1,856 +1,856 @@ -#include -#include -#include -#include "i2c.h" -#include "gc2145.h" - - -void exit(int); - -// astew: Some register names come from https://lwn.net/Articles/905641/ because they're not in the datasheet -// they will have the form e.g. 'GC2145_P0_REG_NAME' - -const -gc_settings_t chip_set_up[] = { - - {0xfe, 0xf0}, // "Reset related" (soft_reset|cm_reset|mipi_reset|CISCTL_restart_n) (page_select=0) - {0xfe, 0xf0}, - {0xfe, 0xf0}, - {0xfc, 0x06}, // "analog_pwc" (vpll_en|vpix_en) - {0xf6, 0x00}, // "Up_dn"/"Pwd_dn" (no pull) - {0xf7, 0x1d}, // "PLL_mode1" (serial_clk_double=1 | clk_double | NA | pll_en) //astew: is this right? I assume NA means.. not applicable? - {0xf8, 0x84}, // "PLL_mode2" (pll_dgdiv_en | divx4=4) - {0xfa, 0x00}, // "clk_div_mode" (0) - {0xf9, 0x8e}, // "cm_mode" (regf_clk_enable | isp_all_clk_enable | serial_clk_enable | re_lock_pll ) - {0xf2, 0x00}, // "pad_vb_hiz_mode"/"data_pad_io"/"sync_pad_io" ( 0 ) - - ///////////////////////////////////////////////// - ////////////////// "System Register" ////////////////////// - //////////////////////////////////////////////////// - {0xfe , 0x00}, // "Reset related" ( page_select=0 ) - {0x03 , 0x04}, // [with next line] - {0x04 , 0xe2}, // "Exposure[12:0]" ( 0x4e2 = 1250 ) - {0x09 , 0x00}, // [with next line] - {0x0a , 0x00}, // "buf_CISCTL_capt_row_start[10:0]" ( 0 ) - {0x0b , 0x00}, // [with next line] - {0x0c , 0x00}, // "buf_CISCTL_capt_col_start[10:0]" ( 0 ) - {0x0d , 0x04}, // [with next line] - {0x0e , 0xc0}, // "buf_CISCTL_capt_win_height" ( 1216 ) - {0x0f , 0x06}, // [with next line] - {0x10 , 0x52}, // "buf_CISCTL_capt_win_width" ( 1618 ) - {0x12 , 0x2e}, // 'GC2145_P0_SH_DELAY_LO' - {0x17 , 0x14}, // "Analog mode1" [sets reserved bits...(only)] - {0x18 , 0x22}, // "Analog mode2" [sets only reserved and "NA" bits] - {0x19 , 0x0e}, // "Analog mode3" [sets only reserved bits] - {0x1a , 0x01}, // ?? - {0x1b , 0x4b}, // ?? - {0x1c , 0x07}, // ?? - {0x1d , 0x10}, // ?? - {0x1e , 0x88}, // ?? - {0x1f , 0x78}, // ?? - {0x20 , 0x03}, // "Analog mode3" [no description] - {0x21 , 0x40}, // [not going to bother saying 'reg not in datasheet' anymore] - {0x22 , 0xa0}, // - {0x24 , 0x16}, // "Driver mode" ( pclk_drv = 16mA | drv_high_data = 12mA | sync_drv = 8mA | drv_low_data = 8mA ) - {0x25 , 0x01}, // ?? - {0x26 , 0x10}, // ?? - {0x2d , 0x60}, // ?? - {0x30 , 0x01}, // ?? - {0x31 , 0x90}, // ?? - {0x33 , 0x06}, // ?? - {0x34 , 0x01}, // ?? - ///////////////////////////////////////////////// - //////////////////ISP reg//////////////////// - ///////////////////////////////////////////////// - {0xfe , 0x00}, // select register page 0 - {0x80 , 0x7f}, // "Block_enable1" - {0x81 , 0x26}, // "Block_enable2" - {0x82 , 0xfa}, // "Block enable" - {0x83 , 0x00}, // "Special effect" - // {0x84 , 0x03}, // "Output format" YUV422 - {0x84 , 0x06}, // "Output format" RGB565 (doesn't actually change output format ofr some reason) - {0x86 , 0x02}, // "Sync mode" - {0x88 , 0x03}, // "module_gating" - {0x89 , 0x03}, // "bypass_mode" - // {0x89 , 0x0B}, // "bypass_mode" - {0x85 , 0x08}, // "Frame start" - {0x8a , 0x00}, // ?? - {0x8b , 0x00}, // ?? - {0xb0 , 0x55}, // "Global_gain" ( 85 ) [datasheet says value is "float 4.4".. UQ4.4?] - {0xc3 , 0x00}, // ?? - {0xc4 , 0x80}, // ?? - {0xc5 , 0x90}, // ?? - {0xc6 , 0x3b}, // ?? - {0xc7 , 0x46}, // ?? - {0xec , 0x06}, // "C_big_win_x0" ( 6 ) // astew: not sure what these actually mean.. - {0xed , 0x04}, // "C_big_win_y0" ( 4 ) - {0xee , 0x60}, // "C_big_win_x1" ( 96 ) - {0xef , 0x90}, // "C_big_win_y1" ( 144 ) - {0xb6 , 0x01}, // ?? - {0x90 , 0x01}, // "Crop enable" (crop out Window mode) - {0x91 , 0x00}, // [with next] - {0x92 , 0x00}, // "out_win_y1[10:0]" ( 0 ) - {0x93 , 0x00}, // [with_next] - {0x94 , 0x00}, // "out_win_x1[10:0]" (0) - {0x95 , 0x04}, // [with next] - {0x96 , 0xb0}, // "out_win_height[10:0]" ( 1200 ) - {0x97 , 0x06}, // [with next] - {0x98 , 0x40}, // "out_win_width[10:0]" ( 1600 ) - ///////////////////////////////////////// - /////////// BLK //////////////////////// - ///////////////////////////////////////// - {0xfe , 0x00}, // select register page 0 - {0x40 , 0x42}, // "Blk_mode1" - {0x41 , 0x00}, // ?? - {0x43 , 0x5b}, // "BLK_fame_cnt_TH" - {0x5e , 0x00}, // "current_G1_offset_odd_ratio" - {0x5f , 0x00}, // "current_G1_offset_even_ratio" - {0x60 , 0x00}, // "current_R1_offset_odd_ratio" - {0x61 , 0x00}, // "current_R1_offset_even_ratio" - {0x62 , 0x00}, // "current_B1_offset_odd_ratio" - {0x63 , 0x00}, // "current_B1_offset_even_ratio" - {0x64 , 0x00}, // "current_G2_offset_odd_ratio" - {0x65 , 0x00}, // "current_G2_offset_even_ratio" - {0x66 , 0x20}, // "Dark_current_G1_ratio" - {0x67 , 0x20}, // "Dark_current_R_ratio" - {0x68 , 0x20}, // "Dark_current_B_ratio" - {0x69 , 0x20}, // "Dark_current_G2_ratio" - {0x76 , 0x00}, // ?? - {0x6a , 0x08}, // "manual_G1_odd_offset" - {0x6b , 0x08}, // "manual_G1_even_offset" - {0x6c , 0x08}, // "manual_R1_odd_offset" - {0x6d , 0x08}, // "manual_R1_even_offset" - {0x6e , 0x08}, // "manual_B2_odd_offset" - {0x6f , 0x08}, // "manual_B2_even_offset" - {0x70 , 0x08}, // "manual_G2_odd_offset" - {0x71 , 0x08}, // "manual_G2_even_offset" - {0x76 , 0x00}, // still ??, as above - {0x72 , 0xf0}, // "BLK_DD_th"/"BLK_various_th" - {0x7e , 0x3c}, // ?? - {0x7f , 0x00}, // ?? - {0xfe , 0x02}, // select register page 2 - {0x48 , 0x15}, // ?? - {0x49 , 0x00}, // ?? - {0x4b , 0x0b}, // ?? - {0xfe , 0x00}, // select register page 0 - //////////////////////////////////////// - /////////// AEC //////////////////////// - //////////////////////////////////////// - {0xfe , 0x01}, // select register page 1 - {0x01 , 0x04}, // "AEC_x1" - {0x02 , 0xc0}, // "AEC_x2" - {0x03 , 0x04}, // "AEC_y1" - {0x04 , 0x90}, // "AEC_y2" - {0x05 , 0x30}, // "AEC_center_x1" - {0x06 , 0x90}, // "AEC_center_x2" - {0x07 , 0x30}, // "AEC_center_y1" - {0x08 , 0x80}, // "AEC_center_y2" - {0x09 , 0x00}, // ?? - {0x0a , 0x82}, // "AEC_mode1" - {0x0b , 0x11}, // "AEC_mode2" - {0x0c , 0x10}, // "AEC_mode3" - {0x11 , 0x10}, // ?? - {0x13 , 0x7b}, // "AEC_target_Y" - {0x17 , 0x00}, // ?? - {0x1c , 0x11}, // ?? - {0x1e , 0x61}, // ?? - {0x1f , 0x35}, // "AEC_max_pre_dg_gain" - {0x20 , 0x40}, // "AEC_max_post_dg_gain" - {0x22 , 0x40}, // ?? - {0x23 , 0x20}, // ?? - {0xfe , 0x02}, // select register page 2 - {0x0f , 0x04}, // ?? - {0xfe , 0x01}, // select register page 1 - {0x12 , 0x35}, // ?? - {0x15 , 0xb0}, // "target_Y_limit_from_histogram" - {0x10 , 0x31}, // ?? - {0x3e , 0x28}, // ?? - {0x3f , 0xb0}, // ?? - {0x40 , 0x90}, // ?? - {0x41 , 0x0f}, // ?? - - ///////////////////////////// - //////// INTPEE ///////////// - ///////////////////////////// - {0xfe , 0x02}, // select register page 2 - {0x90 , 0x6c}, // "EEINTP_mode1" - {0x91 , 0x03}, // "EEINTP_mode2" - {0x92 , 0xcb}, // "direction_TH1" - {0x94 , 0x33}, // "diff_HV_mode" - {0x95 , 0x84}, // "direction_diff_TH_mode" - {0x97 , 0x65}, // "Edge1 effect"/"Edge2 effect" - {0xa2 , 0x11}, // ?? - {0xfe , 0x00}, // select register page 0 - ///////////////////////////// - //////// DNDD/////////////// - ///////////////////////////// - {0xfe , 0x02}, // select register page 2 - {0x80 , 0xc1}, // ?? - {0x81 , 0x08}, // ?? - {0x82 , 0x05}, // ?? - {0x83 , 0x08}, // ?? - {0x84 , 0x0a}, // "DD_dark_th" - {0x86 , 0xf0}, // ?? - {0x87 , 0x50}, // ?? - {0x88 , 0x15}, // ?? - {0x89 , 0xb0}, // "ASDE_low_luma_value_DD_th2" - {0x8a , 0x30}, // "ASDE_low_luma_value_DD_th3" - {0x8b , 0x10}, // "ASDE_low_luma_value_DD_th4" - ///////////////////////////////////////// - /////////// ASDE //////////////////////// - ///////////////////////////////////////// - {0xfe , 0x01}, // select register page 1 - {0x21 , 0x04}, // ?? - {0xfe , 0x02}, // select register page 2 - {0xa3 , 0x50}, // ?? - {0xa4 , 0x20}, // ?? - {0xa5 , 0x40}, // ?? - {0xa6 , 0x80}, // ?? - {0xab , 0x40}, // ?? - {0xae , 0x0c}, // ?? - {0xb3 , 0x46}, // ?? - {0xb4 , 0x64}, // ?? - {0xb6 , 0x38}, // ?? - {0xb7 , 0x01}, // ?? - {0xb9 , 0x2b}, // ?? - {0x3c , 0x04}, // ?? - {0x3d , 0x15}, // ?? - {0x4b , 0x06}, // ?? - {0x4c , 0x20}, // ?? - {0xfe , 0x00}, // select register page 0 - ///////////////////////////////////////// - /////////// GAMMA //////////////////////// - ///////////////////////////////////////// - - ///////////////////gamma1//////////////////// - {0xfe , 0x02}, // select register page 2 - {0x10 , 0x09}, // "Gamma_out1" - {0x11 , 0x0d}, // "Gamma_out2" - {0x12 , 0x13}, // "Gamma_out3" - {0x13 , 0x19}, // "Gamma_out4" - {0x14 , 0x27}, // "Gamma_out5" - {0x15 , 0x37}, // "Gamma_out6" - {0x16 , 0x45}, // "Gamma_out7" - {0x17 , 0x53}, // "Gamma_out8" - {0x18 , 0x69}, // "Gamma_out9" - {0x19 , 0x7d}, // "Gamma_out10" - {0x1a , 0x8f}, // "Gamma_out11" - {0x1b , 0x9d}, // "Gamma_out12" - {0x1c , 0xa9}, // "Gamma_out13" - {0x1d , 0xbd}, // "Gamma_out14" - {0x1e , 0xcd}, // "Gamma_out15" - {0x1f , 0xd9}, // "Gamma_out16" - {0x20 , 0xe3}, // "Gamma_out17" - {0x21 , 0xea}, // "Gamma_out18" - {0x22 , 0xef}, // "Gamma_out19" - {0x23 , 0xf5}, // "Gamma_out20" - {0x24 , 0xf9}, // "Gamma_out21" - {0x25 , 0xff}, // "Gamma_out22" - {0xfe , 0x00}, // select register page 0 - {0xc6 , 0x20}, // ?? - {0xc7 , 0x2b}, // ?? - ///////////////////gamma2//////////////////// - {0xfe , 0x02}, // select register page 2 - {0x26 , 0x0f}, // ?? - {0x27 , 0x14}, // ?? - {0x28 , 0x19}, // ?? - {0x29 , 0x1e}, // ?? - {0x2a , 0x27}, // ?? - {0x2b , 0x33}, // ?? - {0x2c , 0x3b}, // ?? - {0x2d , 0x45}, // ?? - {0x2e , 0x59}, // ?? - {0x2f , 0x69}, // ?? - {0x30 , 0x7c}, // ?? - {0x31 , 0x89}, // ?? - {0x32 , 0x98}, // ?? - {0x33 , 0xae}, // ?? - {0x34 , 0xc0}, // ?? - {0x35 , 0xcf}, // ?? - {0x36 , 0xda}, // ?? - {0x37 , 0xe2}, // ?? - {0x38 , 0xe9}, // ?? - {0x39 , 0xf3}, // ?? - {0x3a , 0xf9}, // ?? - {0x3b , 0xff}, // ?? - /////////////////////////////////////////////// - ///////////YCP /////////////////////// - /////////////////////////////////////////////// - {0xfe , 0x02}, // select register page 2 - {0xd1 , 0x32}, // "saturation_Cb" - {0xd2 , 0x32}, // "saturation_Cr" - {0xd3 , 0x40}, // "luma_contrast" - {0xd6 , 0xf0}, // "skin_Cb_center" - {0xd7 , 0x10}, // "skin_Cr_center" - {0xd8 , 0xda}, // ?? - {0xdd , 0x14}, // ?? - {0xde , 0x86}, // ?? - {0xed , 0x80}, // "C_big_win_y0" - {0xee , 0x00}, // "C_big_win_x1" - {0xef , 0x3f}, // "C_big_win_y1" - {0xd8 , 0xd8}, // ?? - ///////////////////abs///////////////// - {0xfe , 0x01}, // select register page 1 - {0x9f , 0x40}, // ?? - ///////////////////////////////////////////// - //////////////////////// LSC /////////////// - ////////////////////////////////////////// - {0xfe , 0x01}, // select register page 1 - {0xc2 , 0x14}, // LSC_up_red_b2 - {0xc3 , 0x0d}, // LSC_up_green_b2 - {0xc4 , 0x0c}, // LSC_up_blue_b2 - {0xc8 , 0x15}, // LSC_down_red_b2 - {0xc9 , 0x0d}, // LSC_down_green_b2 - {0xca , 0x0a}, // LSC_down_blue_b2 - {0xbc , 0x24}, // LSC_left_red_b2 - {0xbd , 0x10}, // LSC_left_green_b2 - {0xbe , 0x0b}, // LSC_left_blue_b2 - {0xb6 , 0x25}, // LSC_right_red_b2 - {0xb7 , 0x16}, // LSC_right_green_b2 - {0xb8 , 0x15}, // LSC_right_blue_b2 - {0xc5 , 0x00}, // LSC_up_red_b4 - {0xc6 , 0x00}, // LSC_up_green_b4 - {0xc7 , 0x00}, // LSC_up_blue_b4 - {0xcb , 0x00}, // LSC_down_red_b4 - {0xcc , 0x00}, // - {0xcd , 0x00}, // - {0xbf , 0x07}, // - {0xc0 , 0x00}, // - {0xc1 , 0x00}, // - {0xb9 , 0x00}, // - {0xba , 0x00}, // - {0xbb , 0x00}, // - {0xaa , 0x01}, // - {0xab , 0x01}, // - {0xac , 0x00}, // - {0xad , 0x05}, // - {0xae , 0x06}, // - {0xaf , 0x0e}, // - {0xb0 , 0x0b}, // - {0xb1 , 0x07}, // - {0xb2 , 0x06}, // - {0xb3 , 0x17}, // - {0xb4 , 0x0e}, // - {0xb5 , 0x0e}, // - {0xd0 , 0x09}, // - {0xd1 , 0x00}, // - {0xd2 , 0x00}, // - {0xd6 , 0x08}, // - {0xd7 , 0x00}, // - {0xd8 , 0x00}, // - {0xd9 , 0x00}, // - {0xda , 0x00}, // - {0xdb , 0x00}, // - {0xd3 , 0x0a}, // - {0xd4 , 0x00}, // - {0xd5 , 0x00}, // - {0xa4 , 0x00}, // - {0xa5 , 0x00}, // - {0xa6 , 0x77}, // - {0xa7 , 0x77}, // - {0xa8 , 0x77}, // - {0xa9 , 0x77}, // - {0xa1 , 0x80}, // - {0xa2 , 0x80}, // - - {0xfe , 0x01}, // select register page 1 - {0xdf , 0x0d}, - {0xdc , 0x25}, - {0xdd , 0x30}, - {0xe0 , 0x77}, - {0xe1 , 0x80}, - {0xe2 , 0x77}, - {0xe3 , 0x90}, - {0xe6 , 0x90}, - {0xe7 , 0xa0}, - {0xe8 , 0x90}, - {0xe9 , 0xa0}, - {0xfe , 0x00}, // select register page 0 - /////////////////////////////////////////////// - /////////// AWB//////////////////////// - /////////////////////////////////////////////// - {0xfe , 0x01}, // select register page 1 - {0x4f , 0x00}, - {0x4f , 0x00}, - {0x4b , 0x01}, - {0x4f , 0x00}, - - {0x4c , 0x01}, // D75 - {0x4d , 0x71}, - {0x4e , 0x01}, - {0x4c , 0x01}, - {0x4d , 0x91}, - {0x4e , 0x01}, - {0x4c , 0x01}, - {0x4d , 0x70}, - {0x4e , 0x01}, - - {0x4c , 0x01}, // D65 - {0x4d , 0x90}, - {0x4e , 0x02}, - - - {0x4c , 0x01}, - {0x4d , 0xb0}, - {0x4e , 0x02}, - {0x4c , 0x01}, - {0x4d , 0x8f}, - {0x4e , 0x02}, - {0x4c , 0x01}, - {0x4d , 0x6f}, - {0x4e , 0x02}, - {0x4c , 0x01}, - {0x4d , 0xaf}, - {0x4e , 0x02}, - - {0x4c , 0x01}, - {0x4d , 0xd0}, - {0x4e , 0x02}, - {0x4c , 0x01}, - {0x4d , 0xf0}, - {0x4e , 0x02}, - {0x4c , 0x01}, - {0x4d , 0xcf}, - {0x4e , 0x02}, - {0x4c , 0x01}, - {0x4d , 0xef}, - {0x4e , 0x02}, - - {0x4c , 0x01},//D50 - {0x4d , 0x6e}, - {0x4e , 0x03}, - {0x4c , 0x01}, - {0x4d , 0x8e}, - {0x4e , 0x03}, - {0x4c , 0x01}, - {0x4d , 0xae}, - {0x4e , 0x03}, - {0x4c , 0x01}, - {0x4d , 0xce}, - {0x4e , 0x03}, - {0x4c , 0x01}, - {0x4d , 0x4d}, - {0x4e , 0x03}, - {0x4c , 0x01}, - {0x4d , 0x6d}, - {0x4e , 0x03}, - {0x4c , 0x01}, - {0x4d , 0x8d}, - {0x4e , 0x03}, - {0x4c , 0x01}, - {0x4d , 0xad}, - {0x4e , 0x03}, - {0x4c , 0x01}, - {0x4d , 0xcd}, - {0x4e , 0x03}, - {0x4c , 0x01}, - {0x4d , 0x4c}, - {0x4e , 0x03}, - {0x4c , 0x01}, - {0x4d , 0x6c}, - {0x4e , 0x03}, - {0x4c , 0x01}, - {0x4d , 0x8c}, - {0x4e , 0x03}, - {0x4c , 0x01}, - {0x4d , 0xac}, - {0x4e , 0x03}, - {0x4c , 0x01}, - {0x4d , 0xcc}, - {0x4e , 0x03}, - - {0x4c , 0x01}, - {0x4d , 0xcb}, - {0x4e , 0x03}, - - {0x4c , 0x01}, - {0x4d , 0x4b}, - {0x4e , 0x03}, - {0x4c , 0x01}, - {0x4d , 0x6b}, - {0x4e , 0x03}, - {0x4c , 0x01}, - {0x4d , 0x8b}, - {0x4e , 0x03}, - {0x4c , 0x01}, - {0x4d , 0xab}, - {0x4e , 0x03}, - - {0x4c , 0x01},//CWF - {0x4d , 0x8a}, - {0x4e , 0x04}, - {0x4c , 0x01}, - {0x4d , 0xaa}, - {0x4e , 0x04}, - {0x4c , 0x01}, - {0x4d , 0xca}, - {0x4e , 0x04}, - {0x4c , 0x01}, - {0x4d , 0xca}, - {0x4e , 0x04}, - {0x4c , 0x01}, - {0x4d , 0xc9}, - {0x4e , 0x04}, - {0x4c , 0x01}, - {0x4d , 0x8a}, - {0x4e , 0x04}, - {0x4c , 0x01}, - {0x4d , 0x89}, - {0x4e , 0x04}, - {0x4c , 0x01}, - {0x4d , 0xa9}, - {0x4e , 0x04}, - - - - {0x4c , 0x02},//tl84 - {0x4d , 0x0b}, - {0x4e , 0x05}, - {0x4c , 0x02}, - {0x4d , 0x0a}, - {0x4e , 0x05}, - - {0x4c , 0x01}, - {0x4d , 0xeb}, - {0x4e , 0x05}, - - {0x4c , 0x01}, - {0x4d , 0xea}, - {0x4e , 0x05}, - - {0x4c , 0x02}, - {0x4d , 0x09}, - {0x4e , 0x05}, - {0x4c , 0x02}, - {0x4d , 0x29}, - {0x4e , 0x05}, - - {0x4c , 0x02}, - {0x4d , 0x2a}, - {0x4e , 0x05}, - - {0x4c , 0x02}, - {0x4d , 0x4a}, - {0x4e , 0x05}, - - //{0x4c , 0x02}, //A - //{0x4d , 0x6a}, - //{0x4e , 0x06}, - - {0x4c , 0x02}, - {0x4d , 0x8a}, - {0x4e , 0x06}, - - {0x4c , 0x02}, - {0x4d , 0x49}, - {0x4e , 0x06}, - {0x4c , 0x02}, - {0x4d , 0x69}, - {0x4e , 0x06}, - {0x4c , 0x02}, - {0x4d , 0x89}, - {0x4e , 0x06}, - {0x4c , 0x02}, - {0x4d , 0xa9}, - {0x4e , 0x06}, - - {0x4c , 0x02}, - {0x4d , 0x48}, - {0x4e , 0x06}, - {0x4c , 0x02}, - {0x4d , 0x68}, - {0x4e , 0x06}, - {0x4c , 0x02}, - {0x4d , 0x69}, - {0x4e , 0x06}, - - {0x4c , 0x02},//H - {0x4d , 0xca}, - {0x4e , 0x07}, - {0x4c , 0x02}, - {0x4d , 0xc9}, - {0x4e , 0x07}, - {0x4c , 0x02}, - {0x4d , 0xe9}, - {0x4e , 0x07}, - {0x4c , 0x03}, - {0x4d , 0x09}, - {0x4e , 0x07}, - {0x4c , 0x02}, - {0x4d , 0xc8}, - {0x4e , 0x07}, - {0x4c , 0x02}, - {0x4d , 0xe8}, - {0x4e , 0x07}, - {0x4c , 0x02}, - {0x4d , 0xa7}, - {0x4e , 0x07}, - {0x4c , 0x02}, - {0x4d , 0xc7}, - {0x4e , 0x07}, - {0x4c , 0x02}, - {0x4d , 0xe7}, - {0x4e , 0x07}, - {0x4c , 0x03}, - {0x4d , 0x07}, - {0x4e , 0x07}, - - {0x4f , 0x01}, - {0x50 , 0x80}, - {0x51 , 0xa8}, - {0x52 , 0x47}, - {0x53 , 0x38}, - {0x54 , 0xc7}, - {0x56 , 0x0e}, - {0x58 , 0x08}, - {0x5b , 0x00}, - {0x5c , 0x74}, - {0x5d , 0x8b}, - {0x61 , 0xdb}, - {0x62 , 0xb8}, - {0x63 , 0x86}, - {0x64 , 0xc0}, - {0x65 , 0x04}, - - {0x67 , 0xa8}, - {0x68 , 0xb0}, - {0x69 , 0x00}, - {0x6a , 0xa8}, - {0x6b , 0xb0}, - {0x6c , 0xaf}, - {0x6d , 0x8b}, - {0x6e , 0x50}, - {0x6f , 0x18}, - {0x73 , 0xf0}, - {0x70 , 0x0d}, - {0x71 , 0x60}, - {0x72 , 0x80}, - {0x74 , 0x01}, - {0x75 , 0x01}, - {0x7f , 0x0c}, - {0x76 , 0x70}, - {0x77 , 0x58}, - {0x78 , 0xa0}, - {0x79 , 0x5e}, - {0x7a , 0x54}, - {0x7b , 0x58}, - {0xfe , 0x00}, // select register page 0 - ////////////////////////////////////////// - ///////////CC//////////////////////// - ////////////////////////////////////////// - {0xfe , 0x02}, // select register page 2 - {0xc0 , 0x01}, - {0xc1 , 0x44}, - {0xc2 , 0xfd}, - {0xc3 , 0x04}, - {0xc4 , 0xf0}, - {0xc5 , 0x48}, - {0xc6 , 0xfd}, - {0xc7 , 0x46}, - {0xc8 , 0xfd}, - {0xc9 , 0x02}, - {0xca , 0xe0}, - {0xcb , 0x45}, - {0xcc , 0xec}, - {0xcd , 0x48}, - {0xce , 0xf0}, - {0xcf , 0xf0}, - {0xe3 , 0x0c}, - {0xe4 , 0x4b}, - {0xe5 , 0xe0}, - ////////////////////////////////////////// - ///////////ABS //////////////////// - ////////////////////////////////////////// - {0xfe , 0x01}, // select register page 1 - {0x9f , 0x40}, - {0xfe , 0x00}, // select register page 0 - ////////////////////////////////////// - /////////// OUTPUT //////////////// - ////////////////////////////////////// - {0xfe, 0x00}, - {0xf2, 0x00}, - - //////////////frame rate 50Hz///////// - {0xfe , 0x00}, // select register page 0 - {0x05 , 0x01}, - {0x06 , 0x56}, - {0x07 , 0x00}, - {0x08 , 0x32}, - {0xfe , 0x01}, // select register page 1 - {0x25 , 0x00}, - {0x26 , 0xfa}, - {0x27 , 0x04}, - {0x28 , 0xe2}, //20fps - {0x29 , 0x06}, - {0x2a , 0xd6}, //14fps - {0x2b , 0x07}, - {0x2c , 0xd0}, //12fps - {0x2d , 0x0b}, - {0x2e , 0xb8}, //8fps - {0xfe , 0x00}, - - ///////////////dark sun//////////////////// - {0xfe , 0x02}, // select register page 2 - {0x40 , 0xbf}, - {0x46 , 0xcf}, - {0xfe , 0x00}, - ///////////////////////////////////////////////////// - ////////////////////// MIPI ///////////////////// - ///////////////////////////////////////////////////// - {0xfe, 0x03}, // select register page 3 - {0x02, 0x22}, // "DPHY_analog_mode2" (lane0_diff=2; clk_diff=2) - {0x03, 0x10}, // "DPHY_analog_mode3" (clk_delay=1) // 0x12 20140821 - {0x04, 0x10}, // "FIFO_prog_full_level[7:0]" (0x10) // 0x01 - {0x05, 0x00}, // "FIFO_prog_full_level[11:8]" (0x00) - {0x06, 0x88}, // "FIFO_mode" (MIPI_CLK_MODULE | 'read gate') - #ifdef GC2145MIPI_2Lane - {0x01, 0x87}, // "DPHY_analog_mode1" ('clk lane_p2s_sel' | phy_lane1_en | phy_lane0_en | phy_clk_en) - {0x10, 0x95}, // "BUF_CSI2_mode" (lane_enable|MIPI_enable|RAW8|double_lane) - // {0x10, 0x91}, // "BUF_CSI2_mode" (lane_enable|MIPI_enable|double_lane) - #else - {0x01, 0x83}, - {0x10, 0x94}, - #endif - {0x11, 0x1e}, // "LDI_set" - {0x12, 0x80}, // "LWC_set[7:0]" - {0x13, 0x0c}, // "LWC_set[15:8]" - {0x15, 0x10}, // "DPHY_mode" - {0x17, 0xf0}, // "fifo_gate_mode"/"MIPI_wdiv_set" - - {0x21, 0x10}, // "T_LPX_set" - - - {0x22, 0x04}, // "T_CLK_HS_PREPARE_set" - {0x23, 0x10}, // "T_CLK_zero_set" - {0x24, 0x10}, // "T_CLK_PRE_set" - {0x25, 0x10}, // "T_CLK_POST_set" - {0x26, 0x05}, // "T_CLK_TRAIL_set" - {0x29, 0x03}, // "T_HS_PREPARE_set" - {0x2a, 0x0a}, // "T_HS_Zero_set" - {0x2b, 0x06}, // "T_HS_TRAIL_set" - {0xfe, 0x00}, // select register page 0 - - }; - -const size_t chip_set_up_length = sizeof(chip_set_up) / sizeof(chip_set_up[0]); - - -/* -#define INTEGRATION_TIMES 41 -#define ANALOGUE_GAINS 20 -#define DIGITAL_GAINS 25 - -static uint16_t gain_integration_times[INTEGRATION_TIMES] = { - 0x00a, 0x00b, 0x00c, 0x00e, 0x010, 0x012, 0x014, 0x016, 0x019, - 0x01c, 0x020, 0x024, 0x028, 0x02d, 0x033, 0x039, 0x040, 0x048, - 0x051, 0x05b, 0x066, 0x072, 0x080, 0x090, 0x0a2, 0x0b6, 0x0cc, - 0x0e5, 0x101, 0x120, 0x143, 0x16b, 0x197, 0x1c9, 0x201, 0x23f, - 0x286, 0x2d4, 0x32d, 0x390, 0x400, -}; - -static uint8_t gain_analogue_gains[ANALOGUE_GAINS+1] = { - 0, 28, 53, 75, 95, 112, 128, 142, 155, 166, 175, 184, - 192, 199, 205, 211, 215, 220, 224, 227, 231, -}; - -static uint16_t gain_digital_gains[DIGITAL_GAINS+1] = { - 0x0100, 0x011f, 0x0142, 0x0169, 0x0195, 0x01c7, 0x01fe, 0x023d, - 0x0283, 0x02d1, 0x0329, 0x038c, 0x03fb, 0x0477, 0x0503, 0x059f, - 0x064f, 0x0714, 0x07f1, 0x08e9, 0x0a00, 0x0b38, 0x0c96, 0x0e20, - 0x0fd9, -}; - -int gc0310_set_gain_dB(client interface i2c_master_if i2c, - uint32_t dBGain) { - return 0; - uint32_t time, again, dgain; - if (dBGain > GAIN_MAX_DB) { - dBGain = GAIN_MAX_DB; - } - if (dBGain < INTEGRATION_TIMES) { - time = gain_integration_times[dBGain]; - again = gain_analogue_gains[0]; - dgain = gain_digital_gains[0]; - } else { - time = gain_integration_times[INTEGRATION_TIMES-1]; - if (dBGain < INTEGRATION_TIMES + ANALOGUE_GAINS) { - again = gain_analogue_gains[dBGain - INTEGRATION_TIMES + 1]; - dgain = gain_digital_gains[0]; - } else { - again = gain_analogue_gains[ANALOGUE_GAINS]; - dgain = gain_digital_gains[dBGain - INTEGRATION_TIMES - ANALOGUE_GAINS + 1]; - } - } - int ret = i2c_write(i2c, 0x0157, again); - ret |= i2c_write(i2c, 0x0158, dgain >> 8); - ret |= i2c_write(i2c, 0x0159, dgain & 0xff); - ret |= i2c_write(i2c, 0x015A, time >> 8); - ret |= i2c_write(i2c, 0x015B, time & 0xff); - - return ret; -} - -static void GC2145MIPI_Sensor_2M(client interface i2c_master_if i2c) -{ - SENSORDB("GC2145MIPI_Sensor_2M"); - GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x00); - GC2145MIPI_write_cmos_sensor(i2c, 0xfd, 0x00); -#ifdef GC2145MIPI_2Lane - GC2145MIPI_write_cmos_sensor(i2c, 0xfa, 0x00); -#else - GC2145MIPI_write_cmos_sensor(i2c, 0xfa, 0x11); -#endif - //// crop window - GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x00); - GC2145MIPI_write_cmos_sensor(i2c, 0x90, 0x01); - GC2145MIPI_write_cmos_sensor(i2c, 0x91, 0x00); - GC2145MIPI_write_cmos_sensor(i2c, 0x92, 0x00); - GC2145MIPI_write_cmos_sensor(i2c, 0x93, 0x00); - GC2145MIPI_write_cmos_sensor(i2c, 0x94, 0x00); - GC2145MIPI_write_cmos_sensor(i2c, 0x95, 0x04); - GC2145MIPI_write_cmos_sensor(i2c, 0x96, 0xb0); - GC2145MIPI_write_cmos_sensor(i2c, 0x97, 0x06); - GC2145MIPI_write_cmos_sensor(i2c, 0x98, 0x40); - GC2145MIPI_write_cmos_sensor(i2c, 0x99, 0x11); - GC2145MIPI_write_cmos_sensor(i2c, 0x9a, 0x06); - //// AWB - GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x00); - GC2145MIPI_write_cmos_sensor(i2c, 0xec, 0x06); - GC2145MIPI_write_cmos_sensor(i2c, 0xed, 0x04); - GC2145MIPI_write_cmos_sensor(i2c, 0xee, 0x60); - GC2145MIPI_write_cmos_sensor(i2c, 0xef, 0x90); - GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x01); - GC2145MIPI_write_cmos_sensor(i2c, 0x74, 0x01); - //// AEC - GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x01); - GC2145MIPI_write_cmos_sensor(i2c, 0x01, 0x04); - GC2145MIPI_write_cmos_sensor(i2c, 0x02, 0xc0); - GC2145MIPI_write_cmos_sensor(i2c, 0x03, 0x04); - GC2145MIPI_write_cmos_sensor(i2c, 0x04, 0x90); - GC2145MIPI_write_cmos_sensor(i2c, 0x05, 0x30); - GC2145MIPI_write_cmos_sensor(i2c, 0x06, 0x90); - GC2145MIPI_write_cmos_sensor(i2c, 0x07, 0x30); - GC2145MIPI_write_cmos_sensor(i2c, 0x08, 0x80); - GC2145MIPI_write_cmos_sensor(i2c, 0x0a, 0x82); -#ifdef GC2145MIPI_2Lane - GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x01); - GC2145MIPI_write_cmos_sensor(i2c, 0x21, 0x04); - GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x00); - GC2145MIPI_write_cmos_sensor(i2c, 0x20, 0x03); -#else - GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x01); - GC2145MIPI_write_cmos_sensor(i2c, 0x21, 0x15); - GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x00); - GC2145MIPI_write_cmos_sensor(i2c, 0x20, 0x15); -#endif - //// mipi - GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x03); - GC2145MIPI_write_cmos_sensor(i2c, 0x12, 0x80); - GC2145MIPI_write_cmos_sensor(i2c, 0x13, 0x0c); - GC2145MIPI_write_cmos_sensor(i2c, 0x04, 0x01); - GC2145MIPI_write_cmos_sensor(i2c, 0x05, 0x00); - GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x00); - -} - -*/ +#include +#include +#include +#include "i2c.h" +#include "gc2145.h" + + +void exit(int); + +// astew: Some register names come from https://lwn.net/Articles/905641/ because they're not in the datasheet +// they will have the form e.g. 'GC2145_P0_REG_NAME' + +const +gc_settings_t chip_set_up[] = { + + {0xfe, 0xf0}, // "Reset related" (soft_reset|cm_reset|mipi_reset|CISCTL_restart_n) (page_select=0) + {0xfe, 0xf0}, + {0xfe, 0xf0}, + {0xfc, 0x06}, // "analog_pwc" (vpll_en|vpix_en) + {0xf6, 0x00}, // "Up_dn"/"Pwd_dn" (no pull) + {0xf7, 0x1d}, // "PLL_mode1" (serial_clk_double=1 | clk_double | NA | pll_en) //astew: is this right? I assume NA means.. not applicable? + {0xf8, 0x84}, // "PLL_mode2" (pll_dgdiv_en | divx4=4) + {0xfa, 0x00}, // "clk_div_mode" (0) + {0xf9, 0x8e}, // "cm_mode" (regf_clk_enable | isp_all_clk_enable | serial_clk_enable | re_lock_pll ) + {0xf2, 0x00}, // "pad_vb_hiz_mode"/"data_pad_io"/"sync_pad_io" ( 0 ) + + ///////////////////////////////////////////////// + ////////////////// "System Register" ////////////////////// + //////////////////////////////////////////////////// + {0xfe , 0x00}, // "Reset related" ( page_select=0 ) + {0x03 , 0x04}, // [with next line] + {0x04 , 0xe2}, // "Exposure[12:0]" ( 0x4e2 = 1250 ) + {0x09 , 0x00}, // [with next line] + {0x0a , 0x00}, // "buf_CISCTL_capt_row_start[10:0]" ( 0 ) + {0x0b , 0x00}, // [with next line] + {0x0c , 0x00}, // "buf_CISCTL_capt_col_start[10:0]" ( 0 ) + {0x0d , 0x04}, // [with next line] + {0x0e , 0xc0}, // "buf_CISCTL_capt_win_height" ( 1216 ) + {0x0f , 0x06}, // [with next line] + {0x10 , 0x52}, // "buf_CISCTL_capt_win_width" ( 1618 ) + {0x12 , 0x2e}, // 'GC2145_P0_SH_DELAY_LO' + {0x17 , 0x14}, // "Analog mode1" [sets reserved bits...(only)] + {0x18 , 0x22}, // "Analog mode2" [sets only reserved and "NA" bits] + {0x19 , 0x0e}, // "Analog mode3" [sets only reserved bits] + {0x1a , 0x01}, // ?? + {0x1b , 0x4b}, // ?? + {0x1c , 0x07}, // ?? + {0x1d , 0x10}, // ?? + {0x1e , 0x88}, // ?? + {0x1f , 0x78}, // ?? + {0x20 , 0x03}, // "Analog mode3" [no description] + {0x21 , 0x40}, // [not going to bother saying 'reg not in datasheet' anymore] + {0x22 , 0xa0}, // + {0x24 , 0x16}, // "Driver mode" ( pclk_drv = 16mA | drv_high_data = 12mA | sync_drv = 8mA | drv_low_data = 8mA ) + {0x25 , 0x01}, // ?? + {0x26 , 0x10}, // ?? + {0x2d , 0x60}, // ?? + {0x30 , 0x01}, // ?? + {0x31 , 0x90}, // ?? + {0x33 , 0x06}, // ?? + {0x34 , 0x01}, // ?? + ///////////////////////////////////////////////// + //////////////////ISP reg//////////////////// + ///////////////////////////////////////////////// + {0xfe , 0x00}, // select register page 0 + {0x80 , 0x7f}, // "Block_enable1" + {0x81 , 0x26}, // "Block_enable2" + {0x82 , 0xfa}, // "Block enable" + {0x83 , 0x00}, // "Special effect" + // {0x84 , 0x03}, // "Output format" YUV422 + {0x84 , 0x06}, // "Output format" RGB565 (doesn't actually change output format ofr some reason) + {0x86 , 0x02}, // "Sync mode" + {0x88 , 0x03}, // "module_gating" + {0x89 , 0x03}, // "bypass_mode" + // {0x89 , 0x0B}, // "bypass_mode" + {0x85 , 0x08}, // "Frame start" + {0x8a , 0x00}, // ?? + {0x8b , 0x00}, // ?? + {0xb0 , 0x55}, // "Global_gain" ( 85 ) [datasheet says value is "float 4.4".. UQ4.4?] + {0xc3 , 0x00}, // ?? + {0xc4 , 0x80}, // ?? + {0xc5 , 0x90}, // ?? + {0xc6 , 0x3b}, // ?? + {0xc7 , 0x46}, // ?? + {0xec , 0x06}, // "C_big_win_x0" ( 6 ) // astew: not sure what these actually mean.. + {0xed , 0x04}, // "C_big_win_y0" ( 4 ) + {0xee , 0x60}, // "C_big_win_x1" ( 96 ) + {0xef , 0x90}, // "C_big_win_y1" ( 144 ) + {0xb6 , 0x01}, // ?? + {0x90 , 0x01}, // "Crop enable" (crop out Window mode) + {0x91 , 0x00}, // [with next] + {0x92 , 0x00}, // "out_win_y1[10:0]" ( 0 ) + {0x93 , 0x00}, // [with_next] + {0x94 , 0x00}, // "out_win_x1[10:0]" (0) + {0x95 , 0x04}, // [with next] + {0x96 , 0xb0}, // "out_win_height[10:0]" ( 1200 ) + {0x97 , 0x06}, // [with next] + {0x98 , 0x40}, // "out_win_width[10:0]" ( 1600 ) + ///////////////////////////////////////// + /////////// BLK //////////////////////// + ///////////////////////////////////////// + {0xfe , 0x00}, // select register page 0 + {0x40 , 0x42}, // "Blk_mode1" + {0x41 , 0x00}, // ?? + {0x43 , 0x5b}, // "BLK_fame_cnt_TH" + {0x5e , 0x00}, // "current_G1_offset_odd_ratio" + {0x5f , 0x00}, // "current_G1_offset_even_ratio" + {0x60 , 0x00}, // "current_R1_offset_odd_ratio" + {0x61 , 0x00}, // "current_R1_offset_even_ratio" + {0x62 , 0x00}, // "current_B1_offset_odd_ratio" + {0x63 , 0x00}, // "current_B1_offset_even_ratio" + {0x64 , 0x00}, // "current_G2_offset_odd_ratio" + {0x65 , 0x00}, // "current_G2_offset_even_ratio" + {0x66 , 0x20}, // "Dark_current_G1_ratio" + {0x67 , 0x20}, // "Dark_current_R_ratio" + {0x68 , 0x20}, // "Dark_current_B_ratio" + {0x69 , 0x20}, // "Dark_current_G2_ratio" + {0x76 , 0x00}, // ?? + {0x6a , 0x08}, // "manual_G1_odd_offset" + {0x6b , 0x08}, // "manual_G1_even_offset" + {0x6c , 0x08}, // "manual_R1_odd_offset" + {0x6d , 0x08}, // "manual_R1_even_offset" + {0x6e , 0x08}, // "manual_B2_odd_offset" + {0x6f , 0x08}, // "manual_B2_even_offset" + {0x70 , 0x08}, // "manual_G2_odd_offset" + {0x71 , 0x08}, // "manual_G2_even_offset" + {0x76 , 0x00}, // still ??, as above + {0x72 , 0xf0}, // "BLK_DD_th"/"BLK_various_th" + {0x7e , 0x3c}, // ?? + {0x7f , 0x00}, // ?? + {0xfe , 0x02}, // select register page 2 + {0x48 , 0x15}, // ?? + {0x49 , 0x00}, // ?? + {0x4b , 0x0b}, // ?? + {0xfe , 0x00}, // select register page 0 + //////////////////////////////////////// + /////////// AEC //////////////////////// + //////////////////////////////////////// + {0xfe , 0x01}, // select register page 1 + {0x01 , 0x04}, // "AEC_x1" + {0x02 , 0xc0}, // "AEC_x2" + {0x03 , 0x04}, // "AEC_y1" + {0x04 , 0x90}, // "AEC_y2" + {0x05 , 0x30}, // "AEC_center_x1" + {0x06 , 0x90}, // "AEC_center_x2" + {0x07 , 0x30}, // "AEC_center_y1" + {0x08 , 0x80}, // "AEC_center_y2" + {0x09 , 0x00}, // ?? + {0x0a , 0x82}, // "AEC_mode1" + {0x0b , 0x11}, // "AEC_mode2" + {0x0c , 0x10}, // "AEC_mode3" + {0x11 , 0x10}, // ?? + {0x13 , 0x7b}, // "AEC_target_Y" + {0x17 , 0x00}, // ?? + {0x1c , 0x11}, // ?? + {0x1e , 0x61}, // ?? + {0x1f , 0x35}, // "AEC_max_pre_dg_gain" + {0x20 , 0x40}, // "AEC_max_post_dg_gain" + {0x22 , 0x40}, // ?? + {0x23 , 0x20}, // ?? + {0xfe , 0x02}, // select register page 2 + {0x0f , 0x04}, // ?? + {0xfe , 0x01}, // select register page 1 + {0x12 , 0x35}, // ?? + {0x15 , 0xb0}, // "target_Y_limit_from_histogram" + {0x10 , 0x31}, // ?? + {0x3e , 0x28}, // ?? + {0x3f , 0xb0}, // ?? + {0x40 , 0x90}, // ?? + {0x41 , 0x0f}, // ?? + + ///////////////////////////// + //////// INTPEE ///////////// + ///////////////////////////// + {0xfe , 0x02}, // select register page 2 + {0x90 , 0x6c}, // "EEINTP_mode1" + {0x91 , 0x03}, // "EEINTP_mode2" + {0x92 , 0xcb}, // "direction_TH1" + {0x94 , 0x33}, // "diff_HV_mode" + {0x95 , 0x84}, // "direction_diff_TH_mode" + {0x97 , 0x65}, // "Edge1 effect"/"Edge2 effect" + {0xa2 , 0x11}, // ?? + {0xfe , 0x00}, // select register page 0 + ///////////////////////////// + //////// DNDD/////////////// + ///////////////////////////// + {0xfe , 0x02}, // select register page 2 + {0x80 , 0xc1}, // ?? + {0x81 , 0x08}, // ?? + {0x82 , 0x05}, // ?? + {0x83 , 0x08}, // ?? + {0x84 , 0x0a}, // "DD_dark_th" + {0x86 , 0xf0}, // ?? + {0x87 , 0x50}, // ?? + {0x88 , 0x15}, // ?? + {0x89 , 0xb0}, // "ASDE_low_luma_value_DD_th2" + {0x8a , 0x30}, // "ASDE_low_luma_value_DD_th3" + {0x8b , 0x10}, // "ASDE_low_luma_value_DD_th4" + ///////////////////////////////////////// + /////////// ASDE //////////////////////// + ///////////////////////////////////////// + {0xfe , 0x01}, // select register page 1 + {0x21 , 0x04}, // ?? + {0xfe , 0x02}, // select register page 2 + {0xa3 , 0x50}, // ?? + {0xa4 , 0x20}, // ?? + {0xa5 , 0x40}, // ?? + {0xa6 , 0x80}, // ?? + {0xab , 0x40}, // ?? + {0xae , 0x0c}, // ?? + {0xb3 , 0x46}, // ?? + {0xb4 , 0x64}, // ?? + {0xb6 , 0x38}, // ?? + {0xb7 , 0x01}, // ?? + {0xb9 , 0x2b}, // ?? + {0x3c , 0x04}, // ?? + {0x3d , 0x15}, // ?? + {0x4b , 0x06}, // ?? + {0x4c , 0x20}, // ?? + {0xfe , 0x00}, // select register page 0 + ///////////////////////////////////////// + /////////// GAMMA //////////////////////// + ///////////////////////////////////////// + + ///////////////////gamma1//////////////////// + {0xfe , 0x02}, // select register page 2 + {0x10 , 0x09}, // "Gamma_out1" + {0x11 , 0x0d}, // "Gamma_out2" + {0x12 , 0x13}, // "Gamma_out3" + {0x13 , 0x19}, // "Gamma_out4" + {0x14 , 0x27}, // "Gamma_out5" + {0x15 , 0x37}, // "Gamma_out6" + {0x16 , 0x45}, // "Gamma_out7" + {0x17 , 0x53}, // "Gamma_out8" + {0x18 , 0x69}, // "Gamma_out9" + {0x19 , 0x7d}, // "Gamma_out10" + {0x1a , 0x8f}, // "Gamma_out11" + {0x1b , 0x9d}, // "Gamma_out12" + {0x1c , 0xa9}, // "Gamma_out13" + {0x1d , 0xbd}, // "Gamma_out14" + {0x1e , 0xcd}, // "Gamma_out15" + {0x1f , 0xd9}, // "Gamma_out16" + {0x20 , 0xe3}, // "Gamma_out17" + {0x21 , 0xea}, // "Gamma_out18" + {0x22 , 0xef}, // "Gamma_out19" + {0x23 , 0xf5}, // "Gamma_out20" + {0x24 , 0xf9}, // "Gamma_out21" + {0x25 , 0xff}, // "Gamma_out22" + {0xfe , 0x00}, // select register page 0 + {0xc6 , 0x20}, // ?? + {0xc7 , 0x2b}, // ?? + ///////////////////gamma2//////////////////// + {0xfe , 0x02}, // select register page 2 + {0x26 , 0x0f}, // ?? + {0x27 , 0x14}, // ?? + {0x28 , 0x19}, // ?? + {0x29 , 0x1e}, // ?? + {0x2a , 0x27}, // ?? + {0x2b , 0x33}, // ?? + {0x2c , 0x3b}, // ?? + {0x2d , 0x45}, // ?? + {0x2e , 0x59}, // ?? + {0x2f , 0x69}, // ?? + {0x30 , 0x7c}, // ?? + {0x31 , 0x89}, // ?? + {0x32 , 0x98}, // ?? + {0x33 , 0xae}, // ?? + {0x34 , 0xc0}, // ?? + {0x35 , 0xcf}, // ?? + {0x36 , 0xda}, // ?? + {0x37 , 0xe2}, // ?? + {0x38 , 0xe9}, // ?? + {0x39 , 0xf3}, // ?? + {0x3a , 0xf9}, // ?? + {0x3b , 0xff}, // ?? + /////////////////////////////////////////////// + ///////////YCP /////////////////////// + /////////////////////////////////////////////// + {0xfe , 0x02}, // select register page 2 + {0xd1 , 0x32}, // "saturation_Cb" + {0xd2 , 0x32}, // "saturation_Cr" + {0xd3 , 0x40}, // "luma_contrast" + {0xd6 , 0xf0}, // "skin_Cb_center" + {0xd7 , 0x10}, // "skin_Cr_center" + {0xd8 , 0xda}, // ?? + {0xdd , 0x14}, // ?? + {0xde , 0x86}, // ?? + {0xed , 0x80}, // "C_big_win_y0" + {0xee , 0x00}, // "C_big_win_x1" + {0xef , 0x3f}, // "C_big_win_y1" + {0xd8 , 0xd8}, // ?? + ///////////////////abs///////////////// + {0xfe , 0x01}, // select register page 1 + {0x9f , 0x40}, // ?? + ///////////////////////////////////////////// + //////////////////////// LSC /////////////// + ////////////////////////////////////////// + {0xfe , 0x01}, // select register page 1 + {0xc2 , 0x14}, // LSC_up_red_b2 + {0xc3 , 0x0d}, // LSC_up_green_b2 + {0xc4 , 0x0c}, // LSC_up_blue_b2 + {0xc8 , 0x15}, // LSC_down_red_b2 + {0xc9 , 0x0d}, // LSC_down_green_b2 + {0xca , 0x0a}, // LSC_down_blue_b2 + {0xbc , 0x24}, // LSC_left_red_b2 + {0xbd , 0x10}, // LSC_left_green_b2 + {0xbe , 0x0b}, // LSC_left_blue_b2 + {0xb6 , 0x25}, // LSC_right_red_b2 + {0xb7 , 0x16}, // LSC_right_green_b2 + {0xb8 , 0x15}, // LSC_right_blue_b2 + {0xc5 , 0x00}, // LSC_up_red_b4 + {0xc6 , 0x00}, // LSC_up_green_b4 + {0xc7 , 0x00}, // LSC_up_blue_b4 + {0xcb , 0x00}, // LSC_down_red_b4 + {0xcc , 0x00}, // + {0xcd , 0x00}, // + {0xbf , 0x07}, // + {0xc0 , 0x00}, // + {0xc1 , 0x00}, // + {0xb9 , 0x00}, // + {0xba , 0x00}, // + {0xbb , 0x00}, // + {0xaa , 0x01}, // + {0xab , 0x01}, // + {0xac , 0x00}, // + {0xad , 0x05}, // + {0xae , 0x06}, // + {0xaf , 0x0e}, // + {0xb0 , 0x0b}, // + {0xb1 , 0x07}, // + {0xb2 , 0x06}, // + {0xb3 , 0x17}, // + {0xb4 , 0x0e}, // + {0xb5 , 0x0e}, // + {0xd0 , 0x09}, // + {0xd1 , 0x00}, // + {0xd2 , 0x00}, // + {0xd6 , 0x08}, // + {0xd7 , 0x00}, // + {0xd8 , 0x00}, // + {0xd9 , 0x00}, // + {0xda , 0x00}, // + {0xdb , 0x00}, // + {0xd3 , 0x0a}, // + {0xd4 , 0x00}, // + {0xd5 , 0x00}, // + {0xa4 , 0x00}, // + {0xa5 , 0x00}, // + {0xa6 , 0x77}, // + {0xa7 , 0x77}, // + {0xa8 , 0x77}, // + {0xa9 , 0x77}, // + {0xa1 , 0x80}, // + {0xa2 , 0x80}, // + + {0xfe , 0x01}, // select register page 1 + {0xdf , 0x0d}, + {0xdc , 0x25}, + {0xdd , 0x30}, + {0xe0 , 0x77}, + {0xe1 , 0x80}, + {0xe2 , 0x77}, + {0xe3 , 0x90}, + {0xe6 , 0x90}, + {0xe7 , 0xa0}, + {0xe8 , 0x90}, + {0xe9 , 0xa0}, + {0xfe , 0x00}, // select register page 0 + /////////////////////////////////////////////// + /////////// AWB//////////////////////// + /////////////////////////////////////////////// + {0xfe , 0x01}, // select register page 1 + {0x4f , 0x00}, + {0x4f , 0x00}, + {0x4b , 0x01}, + {0x4f , 0x00}, + + {0x4c , 0x01}, // D75 + {0x4d , 0x71}, + {0x4e , 0x01}, + {0x4c , 0x01}, + {0x4d , 0x91}, + {0x4e , 0x01}, + {0x4c , 0x01}, + {0x4d , 0x70}, + {0x4e , 0x01}, + + {0x4c , 0x01}, // D65 + {0x4d , 0x90}, + {0x4e , 0x02}, + + + {0x4c , 0x01}, + {0x4d , 0xb0}, + {0x4e , 0x02}, + {0x4c , 0x01}, + {0x4d , 0x8f}, + {0x4e , 0x02}, + {0x4c , 0x01}, + {0x4d , 0x6f}, + {0x4e , 0x02}, + {0x4c , 0x01}, + {0x4d , 0xaf}, + {0x4e , 0x02}, + + {0x4c , 0x01}, + {0x4d , 0xd0}, + {0x4e , 0x02}, + {0x4c , 0x01}, + {0x4d , 0xf0}, + {0x4e , 0x02}, + {0x4c , 0x01}, + {0x4d , 0xcf}, + {0x4e , 0x02}, + {0x4c , 0x01}, + {0x4d , 0xef}, + {0x4e , 0x02}, + + {0x4c , 0x01},//D50 + {0x4d , 0x6e}, + {0x4e , 0x03}, + {0x4c , 0x01}, + {0x4d , 0x8e}, + {0x4e , 0x03}, + {0x4c , 0x01}, + {0x4d , 0xae}, + {0x4e , 0x03}, + {0x4c , 0x01}, + {0x4d , 0xce}, + {0x4e , 0x03}, + {0x4c , 0x01}, + {0x4d , 0x4d}, + {0x4e , 0x03}, + {0x4c , 0x01}, + {0x4d , 0x6d}, + {0x4e , 0x03}, + {0x4c , 0x01}, + {0x4d , 0x8d}, + {0x4e , 0x03}, + {0x4c , 0x01}, + {0x4d , 0xad}, + {0x4e , 0x03}, + {0x4c , 0x01}, + {0x4d , 0xcd}, + {0x4e , 0x03}, + {0x4c , 0x01}, + {0x4d , 0x4c}, + {0x4e , 0x03}, + {0x4c , 0x01}, + {0x4d , 0x6c}, + {0x4e , 0x03}, + {0x4c , 0x01}, + {0x4d , 0x8c}, + {0x4e , 0x03}, + {0x4c , 0x01}, + {0x4d , 0xac}, + {0x4e , 0x03}, + {0x4c , 0x01}, + {0x4d , 0xcc}, + {0x4e , 0x03}, + + {0x4c , 0x01}, + {0x4d , 0xcb}, + {0x4e , 0x03}, + + {0x4c , 0x01}, + {0x4d , 0x4b}, + {0x4e , 0x03}, + {0x4c , 0x01}, + {0x4d , 0x6b}, + {0x4e , 0x03}, + {0x4c , 0x01}, + {0x4d , 0x8b}, + {0x4e , 0x03}, + {0x4c , 0x01}, + {0x4d , 0xab}, + {0x4e , 0x03}, + + {0x4c , 0x01},//CWF + {0x4d , 0x8a}, + {0x4e , 0x04}, + {0x4c , 0x01}, + {0x4d , 0xaa}, + {0x4e , 0x04}, + {0x4c , 0x01}, + {0x4d , 0xca}, + {0x4e , 0x04}, + {0x4c , 0x01}, + {0x4d , 0xca}, + {0x4e , 0x04}, + {0x4c , 0x01}, + {0x4d , 0xc9}, + {0x4e , 0x04}, + {0x4c , 0x01}, + {0x4d , 0x8a}, + {0x4e , 0x04}, + {0x4c , 0x01}, + {0x4d , 0x89}, + {0x4e , 0x04}, + {0x4c , 0x01}, + {0x4d , 0xa9}, + {0x4e , 0x04}, + + + + {0x4c , 0x02},//tl84 + {0x4d , 0x0b}, + {0x4e , 0x05}, + {0x4c , 0x02}, + {0x4d , 0x0a}, + {0x4e , 0x05}, + + {0x4c , 0x01}, + {0x4d , 0xeb}, + {0x4e , 0x05}, + + {0x4c , 0x01}, + {0x4d , 0xea}, + {0x4e , 0x05}, + + {0x4c , 0x02}, + {0x4d , 0x09}, + {0x4e , 0x05}, + {0x4c , 0x02}, + {0x4d , 0x29}, + {0x4e , 0x05}, + + {0x4c , 0x02}, + {0x4d , 0x2a}, + {0x4e , 0x05}, + + {0x4c , 0x02}, + {0x4d , 0x4a}, + {0x4e , 0x05}, + + //{0x4c , 0x02}, //A + //{0x4d , 0x6a}, + //{0x4e , 0x06}, + + {0x4c , 0x02}, + {0x4d , 0x8a}, + {0x4e , 0x06}, + + {0x4c , 0x02}, + {0x4d , 0x49}, + {0x4e , 0x06}, + {0x4c , 0x02}, + {0x4d , 0x69}, + {0x4e , 0x06}, + {0x4c , 0x02}, + {0x4d , 0x89}, + {0x4e , 0x06}, + {0x4c , 0x02}, + {0x4d , 0xa9}, + {0x4e , 0x06}, + + {0x4c , 0x02}, + {0x4d , 0x48}, + {0x4e , 0x06}, + {0x4c , 0x02}, + {0x4d , 0x68}, + {0x4e , 0x06}, + {0x4c , 0x02}, + {0x4d , 0x69}, + {0x4e , 0x06}, + + {0x4c , 0x02},//H + {0x4d , 0xca}, + {0x4e , 0x07}, + {0x4c , 0x02}, + {0x4d , 0xc9}, + {0x4e , 0x07}, + {0x4c , 0x02}, + {0x4d , 0xe9}, + {0x4e , 0x07}, + {0x4c , 0x03}, + {0x4d , 0x09}, + {0x4e , 0x07}, + {0x4c , 0x02}, + {0x4d , 0xc8}, + {0x4e , 0x07}, + {0x4c , 0x02}, + {0x4d , 0xe8}, + {0x4e , 0x07}, + {0x4c , 0x02}, + {0x4d , 0xa7}, + {0x4e , 0x07}, + {0x4c , 0x02}, + {0x4d , 0xc7}, + {0x4e , 0x07}, + {0x4c , 0x02}, + {0x4d , 0xe7}, + {0x4e , 0x07}, + {0x4c , 0x03}, + {0x4d , 0x07}, + {0x4e , 0x07}, + + {0x4f , 0x01}, + {0x50 , 0x80}, + {0x51 , 0xa8}, + {0x52 , 0x47}, + {0x53 , 0x38}, + {0x54 , 0xc7}, + {0x56 , 0x0e}, + {0x58 , 0x08}, + {0x5b , 0x00}, + {0x5c , 0x74}, + {0x5d , 0x8b}, + {0x61 , 0xdb}, + {0x62 , 0xb8}, + {0x63 , 0x86}, + {0x64 , 0xc0}, + {0x65 , 0x04}, + + {0x67 , 0xa8}, + {0x68 , 0xb0}, + {0x69 , 0x00}, + {0x6a , 0xa8}, + {0x6b , 0xb0}, + {0x6c , 0xaf}, + {0x6d , 0x8b}, + {0x6e , 0x50}, + {0x6f , 0x18}, + {0x73 , 0xf0}, + {0x70 , 0x0d}, + {0x71 , 0x60}, + {0x72 , 0x80}, + {0x74 , 0x01}, + {0x75 , 0x01}, + {0x7f , 0x0c}, + {0x76 , 0x70}, + {0x77 , 0x58}, + {0x78 , 0xa0}, + {0x79 , 0x5e}, + {0x7a , 0x54}, + {0x7b , 0x58}, + {0xfe , 0x00}, // select register page 0 + ////////////////////////////////////////// + ///////////CC//////////////////////// + ////////////////////////////////////////// + {0xfe , 0x02}, // select register page 2 + {0xc0 , 0x01}, + {0xc1 , 0x44}, + {0xc2 , 0xfd}, + {0xc3 , 0x04}, + {0xc4 , 0xf0}, + {0xc5 , 0x48}, + {0xc6 , 0xfd}, + {0xc7 , 0x46}, + {0xc8 , 0xfd}, + {0xc9 , 0x02}, + {0xca , 0xe0}, + {0xcb , 0x45}, + {0xcc , 0xec}, + {0xcd , 0x48}, + {0xce , 0xf0}, + {0xcf , 0xf0}, + {0xe3 , 0x0c}, + {0xe4 , 0x4b}, + {0xe5 , 0xe0}, + ////////////////////////////////////////// + ///////////ABS //////////////////// + ////////////////////////////////////////// + {0xfe , 0x01}, // select register page 1 + {0x9f , 0x40}, + {0xfe , 0x00}, // select register page 0 + ////////////////////////////////////// + /////////// OUTPUT //////////////// + ////////////////////////////////////// + {0xfe, 0x00}, + {0xf2, 0x00}, + + //////////////frame rate 50Hz///////// + {0xfe , 0x00}, // select register page 0 + {0x05 , 0x01}, + {0x06 , 0x56}, + {0x07 , 0x00}, + {0x08 , 0x32}, + {0xfe , 0x01}, // select register page 1 + {0x25 , 0x00}, + {0x26 , 0xfa}, + {0x27 , 0x04}, + {0x28 , 0xe2}, //20fps + {0x29 , 0x06}, + {0x2a , 0xd6}, //14fps + {0x2b , 0x07}, + {0x2c , 0xd0}, //12fps + {0x2d , 0x0b}, + {0x2e , 0xb8}, //8fps + {0xfe , 0x00}, + + ///////////////dark sun//////////////////// + {0xfe , 0x02}, // select register page 2 + {0x40 , 0xbf}, + {0x46 , 0xcf}, + {0xfe , 0x00}, + ///////////////////////////////////////////////////// + ////////////////////// MIPI ///////////////////// + ///////////////////////////////////////////////////// + {0xfe, 0x03}, // select register page 3 + {0x02, 0x22}, // "DPHY_analog_mode2" (lane0_diff=2; clk_diff=2) + {0x03, 0x10}, // "DPHY_analog_mode3" (clk_delay=1) // 0x12 20140821 + {0x04, 0x10}, // "FIFO_prog_full_level[7:0]" (0x10) // 0x01 + {0x05, 0x00}, // "FIFO_prog_full_level[11:8]" (0x00) + {0x06, 0x88}, // "FIFO_mode" (MIPI_CLK_MODULE | 'read gate') + #ifdef GC2145MIPI_2Lane + {0x01, 0x87}, // "DPHY_analog_mode1" ('clk lane_p2s_sel' | phy_lane1_en | phy_lane0_en | phy_clk_en) + {0x10, 0x95}, // "BUF_CSI2_mode" (lane_enable|MIPI_enable|RAW8|double_lane) + // {0x10, 0x91}, // "BUF_CSI2_mode" (lane_enable|MIPI_enable|double_lane) + #else + {0x01, 0x83}, + {0x10, 0x94}, + #endif + {0x11, 0x1e}, // "LDI_set" + {0x12, 0x80}, // "LWC_set[7:0]" + {0x13, 0x0c}, // "LWC_set[15:8]" + {0x15, 0x10}, // "DPHY_mode" + {0x17, 0xf0}, // "fifo_gate_mode"/"MIPI_wdiv_set" + + {0x21, 0x10}, // "T_LPX_set" + + + {0x22, 0x04}, // "T_CLK_HS_PREPARE_set" + {0x23, 0x10}, // "T_CLK_zero_set" + {0x24, 0x10}, // "T_CLK_PRE_set" + {0x25, 0x10}, // "T_CLK_POST_set" + {0x26, 0x05}, // "T_CLK_TRAIL_set" + {0x29, 0x03}, // "T_HS_PREPARE_set" + {0x2a, 0x0a}, // "T_HS_Zero_set" + {0x2b, 0x06}, // "T_HS_TRAIL_set" + {0xfe, 0x00}, // select register page 0 + + }; + +const size_t chip_set_up_length = sizeof(chip_set_up) / sizeof(chip_set_up[0]); + + +/* +#define INTEGRATION_TIMES 41 +#define ANALOGUE_GAINS 20 +#define DIGITAL_GAINS 25 + +static uint16_t gain_integration_times[INTEGRATION_TIMES] = { + 0x00a, 0x00b, 0x00c, 0x00e, 0x010, 0x012, 0x014, 0x016, 0x019, + 0x01c, 0x020, 0x024, 0x028, 0x02d, 0x033, 0x039, 0x040, 0x048, + 0x051, 0x05b, 0x066, 0x072, 0x080, 0x090, 0x0a2, 0x0b6, 0x0cc, + 0x0e5, 0x101, 0x120, 0x143, 0x16b, 0x197, 0x1c9, 0x201, 0x23f, + 0x286, 0x2d4, 0x32d, 0x390, 0x400, +}; + +static uint8_t gain_analogue_gains[ANALOGUE_GAINS+1] = { + 0, 28, 53, 75, 95, 112, 128, 142, 155, 166, 175, 184, + 192, 199, 205, 211, 215, 220, 224, 227, 231, +}; + +static uint16_t gain_digital_gains[DIGITAL_GAINS+1] = { + 0x0100, 0x011f, 0x0142, 0x0169, 0x0195, 0x01c7, 0x01fe, 0x023d, + 0x0283, 0x02d1, 0x0329, 0x038c, 0x03fb, 0x0477, 0x0503, 0x059f, + 0x064f, 0x0714, 0x07f1, 0x08e9, 0x0a00, 0x0b38, 0x0c96, 0x0e20, + 0x0fd9, +}; + +int gc0310_set_gain_dB(client interface i2c_master_if i2c, + uint32_t dBGain) { + return 0; + uint32_t time, again, dgain; + if (dBGain > GAIN_MAX_DB) { + dBGain = GAIN_MAX_DB; + } + if (dBGain < INTEGRATION_TIMES) { + time = gain_integration_times[dBGain]; + again = gain_analogue_gains[0]; + dgain = gain_digital_gains[0]; + } else { + time = gain_integration_times[INTEGRATION_TIMES-1]; + if (dBGain < INTEGRATION_TIMES + ANALOGUE_GAINS) { + again = gain_analogue_gains[dBGain - INTEGRATION_TIMES + 1]; + dgain = gain_digital_gains[0]; + } else { + again = gain_analogue_gains[ANALOGUE_GAINS]; + dgain = gain_digital_gains[dBGain - INTEGRATION_TIMES - ANALOGUE_GAINS + 1]; + } + } + int ret = i2c_write(i2c, 0x0157, again); + ret |= i2c_write(i2c, 0x0158, dgain >> 8); + ret |= i2c_write(i2c, 0x0159, dgain & 0xff); + ret |= i2c_write(i2c, 0x015A, time >> 8); + ret |= i2c_write(i2c, 0x015B, time & 0xff); + + return ret; +} + +static void GC2145MIPI_Sensor_2M(client interface i2c_master_if i2c) +{ + SENSORDB("GC2145MIPI_Sensor_2M"); + GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x00); + GC2145MIPI_write_cmos_sensor(i2c, 0xfd, 0x00); +#ifdef GC2145MIPI_2Lane + GC2145MIPI_write_cmos_sensor(i2c, 0xfa, 0x00); +#else + GC2145MIPI_write_cmos_sensor(i2c, 0xfa, 0x11); +#endif + //// crop window + GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x00); + GC2145MIPI_write_cmos_sensor(i2c, 0x90, 0x01); + GC2145MIPI_write_cmos_sensor(i2c, 0x91, 0x00); + GC2145MIPI_write_cmos_sensor(i2c, 0x92, 0x00); + GC2145MIPI_write_cmos_sensor(i2c, 0x93, 0x00); + GC2145MIPI_write_cmos_sensor(i2c, 0x94, 0x00); + GC2145MIPI_write_cmos_sensor(i2c, 0x95, 0x04); + GC2145MIPI_write_cmos_sensor(i2c, 0x96, 0xb0); + GC2145MIPI_write_cmos_sensor(i2c, 0x97, 0x06); + GC2145MIPI_write_cmos_sensor(i2c, 0x98, 0x40); + GC2145MIPI_write_cmos_sensor(i2c, 0x99, 0x11); + GC2145MIPI_write_cmos_sensor(i2c, 0x9a, 0x06); + //// AWB + GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x00); + GC2145MIPI_write_cmos_sensor(i2c, 0xec, 0x06); + GC2145MIPI_write_cmos_sensor(i2c, 0xed, 0x04); + GC2145MIPI_write_cmos_sensor(i2c, 0xee, 0x60); + GC2145MIPI_write_cmos_sensor(i2c, 0xef, 0x90); + GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x01); + GC2145MIPI_write_cmos_sensor(i2c, 0x74, 0x01); + //// AEC + GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x01); + GC2145MIPI_write_cmos_sensor(i2c, 0x01, 0x04); + GC2145MIPI_write_cmos_sensor(i2c, 0x02, 0xc0); + GC2145MIPI_write_cmos_sensor(i2c, 0x03, 0x04); + GC2145MIPI_write_cmos_sensor(i2c, 0x04, 0x90); + GC2145MIPI_write_cmos_sensor(i2c, 0x05, 0x30); + GC2145MIPI_write_cmos_sensor(i2c, 0x06, 0x90); + GC2145MIPI_write_cmos_sensor(i2c, 0x07, 0x30); + GC2145MIPI_write_cmos_sensor(i2c, 0x08, 0x80); + GC2145MIPI_write_cmos_sensor(i2c, 0x0a, 0x82); +#ifdef GC2145MIPI_2Lane + GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x01); + GC2145MIPI_write_cmos_sensor(i2c, 0x21, 0x04); + GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x00); + GC2145MIPI_write_cmos_sensor(i2c, 0x20, 0x03); +#else + GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x01); + GC2145MIPI_write_cmos_sensor(i2c, 0x21, 0x15); + GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x00); + GC2145MIPI_write_cmos_sensor(i2c, 0x20, 0x15); +#endif + //// mipi + GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x03); + GC2145MIPI_write_cmos_sensor(i2c, 0x12, 0x80); + GC2145MIPI_write_cmos_sensor(i2c, 0x13, 0x0c); + GC2145MIPI_write_cmos_sensor(i2c, 0x04, 0x01); + GC2145MIPI_write_cmos_sensor(i2c, 0x05, 0x00); + GC2145MIPI_write_cmos_sensor(i2c, 0xfe, 0x00); + +} + +*/ diff --git a/sensors/galaxycore_gc2145/readme.rst b/sensors/galaxycore_gc2145/readme.rst new file mode 100644 index 00000000..cd88eba5 --- /dev/null +++ b/sensors/galaxycore_gc2145/readme.rst @@ -0,0 +1,25 @@ +===================== +Galaxy Core GC2145 Sensor +===================== + +The Galaxy Core GC2145 sensor is an image sensor developed by Galaxy Core Inc., commonly used in camera modules for mobile phones and other electronic devices. + +Key Features +------------ + +- Optical Format: 1/5-inch +- Resolution: 2 megapixels +- Technology: CMOS (Complementary Metal-Oxide-Semiconductor) +- Image Processing: Basic image processing capabilities +- Application: Mobile phone camera modules and compact electronic devices + +Description +------------ + +The Galaxy Core GC2145 sensor is designed for compact imaging solutions requiring modest resolution capabilities. With a 1/5-inch optical format and a resolution of 2 megapixels, it provides adequate image quality for everyday photography needs. + +Utilizing CMOS technology, the GC2145 sensor offers a cost-effective solution with low power consumption. While it may not deliver the same level of performance as higher-resolution sensors, it is still capable of capturing decent images and videos for general-purpose use. + +The GC2145 sensor incorporates basic image processing capabilities to enhance the captured images. While not as advanced as some other sensors on the market, these processing features help improve color reproduction, contrast, and sharpness to some extent. + +Mobile phone manufacturers often employ the Galaxy Core GC2145 sensor in their camera modules for entry-level and mid-range devices. Its compact size, cost-effectiveness, and reasonable image quality make it a suitable choice for these applications. diff --git a/sensors/imx219.h b/sensors/sony_imx219/imx219.h similarity index 96% rename from sensors/imx219.h rename to sensors/sony_imx219/imx219.h index f95758eb..54d1e07f 100644 --- a/sensors/imx219.h +++ b/sensors/sony_imx219/imx219.h @@ -1,73 +1,73 @@ -#pragma once - -#include "sensor.h" - -#include -#include "i2c.h" -#include "xccompat.h" - -// I2C adress -#define IMX219_I2C_ADDR 0x10 - -// Imx settings -typedef struct -{ - uint16_t addr; - uint16_t val; -} imx219_settings_t; - -// configure registers -#if ((CONFIG_MODE == 0) || (CONFIG_MODE == 1)) - #define CONFIG_REG mode_640_480_regs -#elif (CONFIG_MODE == 2) - #define CONFIG_REG mode_1640_1232_regs -#else - #error "Invalid configuration mode" -#endif - -// Configure formats -#if EXPECTED_FORMAT == MIPI_DT_RAW10 - #define DATA_FORMAT_REGS raw10_framefmt_regs - -#elif EXPECTED_FORMAT == MIPI_DT_RAW8 - #define DATA_FORMAT_REGS raw8_framefmt_regs - -#endif - -// configure FPS -#if defined(FPS_13) - #define PLL_VT_MPY 0x0030 -#elif defined(FPS_24) - #define PLL_VT_MPY 0x0047 -#elif defined(FPS_30) - #define PLL_VT_MPY 0x0058 -#elif defined(FPS_38) - #define PLL_VT_MPY 0x0070 -#elif defined(FPS_53) - #define PLL_VT_MPY 0x009C -#elif defined(FPS_76) - #define PLL_VT_MPY 0x00E0 -#else - #warning fps not defined, selecting default value - #define PLL_VT_MPY 0x0027 -#endif - - -// functions -int imx219_init(CLIENT_INTERFACE(i2c_master_if, i2c)); -int imx219_stream_start(CLIENT_INTERFACE(i2c_master_if, i2c)); -int imx219_configure_mode(CLIENT_INTERFACE(i2c_master_if, i2c)); -int imx219_stream_stop(CLIENT_INTERFACE(i2c_master_if, i2c)); -int imx219_set_gain_dB(CLIENT_INTERFACE(i2c_master_if, i2c), uint32_t dBGain); -int imx219_set_binning(CLIENT_INTERFACE(i2c_master_if, i2c), uint32_t H_binning, uint32_t V_binning); -int imx219_read(CLIENT_INTERFACE(i2c_master_if, i2c), uint16_t addr); -void imx219_read_gains(CLIENT_INTERFACE(i2c_master_if, i2c), uint16_t values[5]); - - -#define camera_init(X) imx219_init(X) -#define camera_start(X) imx219_stream_start(X) -#define camera_configure(X) imx219_configure_mode(X) -#define camera_set_exposure(iic,ex) imx219_set_gain_dB(iic,ex) - - - +#pragma once + +#include "sensor.h" + +#include +#include "i2c.h" +#include "xccompat.h" + +// I2C adress +#define IMX219_I2C_ADDR 0x10 + +// Imx settings +typedef struct +{ + uint16_t addr; + uint16_t val; +} imx219_settings_t; + +// configure registers +#if ((CONFIG_MODE == 0) || (CONFIG_MODE == 1)) + #define CONFIG_REG mode_640_480_regs +#elif (CONFIG_MODE == 2) + #define CONFIG_REG mode_1640_1232_regs +#else + #error "Invalid configuration mode" +#endif + +// Configure formats +#if EXPECTED_FORMAT == MIPI_DT_RAW10 + #define DATA_FORMAT_REGS raw10_framefmt_regs + +#elif EXPECTED_FORMAT == MIPI_DT_RAW8 + #define DATA_FORMAT_REGS raw8_framefmt_regs + +#endif + +// configure FPS +#if defined(FPS_13) + #define PLL_VT_MPY 0x0030 +#elif defined(FPS_24) + #define PLL_VT_MPY 0x0047 +#elif defined(FPS_30) + #define PLL_VT_MPY 0x0058 +#elif defined(FPS_38) + #define PLL_VT_MPY 0x0070 +#elif defined(FPS_53) + #define PLL_VT_MPY 0x009C +#elif defined(FPS_76) + #define PLL_VT_MPY 0x00E0 +#else + #warning fps not defined, selecting default value + #define PLL_VT_MPY 0x0027 +#endif + + +// functions +int imx219_init(CLIENT_INTERFACE(i2c_master_if, i2c)); +int imx219_stream_start(CLIENT_INTERFACE(i2c_master_if, i2c)); +int imx219_configure_mode(CLIENT_INTERFACE(i2c_master_if, i2c)); +int imx219_stream_stop(CLIENT_INTERFACE(i2c_master_if, i2c)); +int imx219_set_gain_dB(CLIENT_INTERFACE(i2c_master_if, i2c), uint32_t dBGain); +int imx219_set_binning(CLIENT_INTERFACE(i2c_master_if, i2c), uint32_t H_binning, uint32_t V_binning); +int imx219_read(CLIENT_INTERFACE(i2c_master_if, i2c), uint16_t addr); +void imx219_read_gains(CLIENT_INTERFACE(i2c_master_if, i2c), uint16_t values[5]); + + +#define camera_init(X) imx219_init(X) +#define camera_start(X) imx219_stream_start(X) +#define camera_configure(X) imx219_configure_mode(X) +#define camera_set_exposure(iic,ex) imx219_set_gain_dB(iic,ex) + + + diff --git a/sensors/imx219.xc b/sensors/sony_imx219/imx219.xc similarity index 96% rename from sensors/imx219.xc rename to sensors/sony_imx219/imx219.xc index 44f957bb..055fa876 100644 --- a/sensors/imx219.xc +++ b/sensors/sony_imx219/imx219.xc @@ -1,182 +1,182 @@ -#include -#include -#include - -#include -#include "i2c.h" -#include "imx219.h" -#include "imx219_reg.h" - -#define GAIN_DB 40 - -static int i2c_write(client interface i2c_master_if i2c, int reg, int value) -{ - i2c_regop_res_t result; - // Write an 8-bit register on a slave device from a 16-bit register address. - result = i2c.write_reg8_addr16(IMX219_I2C_ADDR, reg, value); - if (result != I2C_REGOP_SUCCESS) - { - // printf("Failed on address %02x value %02x\n", reg, value); - // TODO FIXME - } - return result != I2C_REGOP_SUCCESS ? -1 : 0; -} - -static int i2c_write_table(client interface i2c_master_if i2c, - imx219_settings_t table[], - int N) { - int ret; - for(int i = 0; i < N; i++) { - uint32_t address = table[i].addr; - uint32_t value = table[i].val; - if (address == SLEEP) { - timer tmr; - int t; - tmr :> t; - tmr when timerafter(t + TRSTUS * 100) :> void; - } - if (address & 0x8000) { - address &= 0x7fff; - ret = i2c_write(i2c, address, value >> 8); // B1 B2 B3 B4 -> B1 B2 - ret |= i2c_write(i2c, address+1, value & 0xff); // B1 B2 B3 B4 -> B3 B4 - } else { - ret = i2c_write(i2c, address, value); - } - if (ret < 0) { - return ret; - } - } - return 0; -} - - -static int i2c_write_table_val(client interface i2c_master_if i2c, - imx219_settings_t table[], - int N) { - int ret; - char mode = 's'; - - for(int i = 0; i < N; i++) { - uint32_t address = table[i].addr; - uint32_t value = table[i].val; - if (address == SLEEP) { - timer tmr; - int t; - tmr :> t; - tmr when timerafter(t + TRSTUS * 100) :> void; - } - - if ((value & 0xFF00) || (address & 0x8000)){ - if (address & 0x8000) { - address &= 0x7fff; - } - mode = 'c'; - //printf("mode=%c , address = 0x%04x, value = 0x%02x\n", mode, address, value >> 8); - //printf("mode=%c , address+ = 0x%04x, value = 0x%02x\n", mode, address+1, value & 0xff); - // continous writte - ret = i2c_write(i2c, address, value >> 8); // B1 B2 B3 B4 -> B1 B2 - ret |= i2c_write(i2c, address+1, value & 0xff); // B1 B2 B3 B4 -> B3 B4 - } - else - { - // single writte - mode = 's'; - ret = i2c_write(i2c, address, value); - //printf("mode=%c , address = 0x%04x, value = 0x%02x\n", mode, address, value); - } - if (ret < 0) { - return ret; - } - } - return 0; -} - - -int imx219_read(client interface i2c_master_if i2c, uint16_t addr){ - i2c_regop_res_t res; - uint16_t val = i2c.read_reg16(IMX219_I2C_ADDR, addr, res); - assert(res == 0); - return val; -} - -void imx219_read_gains(client interface i2c_master_if i2c, uint16_t values[5]){ - values[0] = imx219_read(i2c, 0x0157); - values[1] = imx219_read(i2c, 0x0158); - values[2] = imx219_read(i2c, 0x0159); - values[3] = imx219_read(i2c, 0x015A); - values[4] = imx219_read(i2c, 0x015B); -} - -/// ------------------------------------------------------------------------------- -int imx219_init(client interface i2c_master_if i2c) -{ - int ret = 0; - // Send all registers that are common to all modes - ret = i2c_write_table_val(i2c, imx219_common_regs, sizeof(imx219_common_regs) / sizeof(imx219_common_regs[0])); - // Configure two or four Lane mode - ret = i2c_write_table_val(i2c, imx219_lanes_regs, sizeof(imx219_lanes_regs) / sizeof(imx219_lanes_regs[0])); - // set gain - ret = imx219_set_gain_dB(i2c, GAIN_DB); - return ret; -} - -int imx219_configure_mode(client interface i2c_master_if i2c) -{ - int ret = 0; - // Apply default values of current mode - ret = i2c_write_table_val(i2c, CONFIG_REG, sizeof(CONFIG_REG) / sizeof(CONFIG_REG[0])); - // set frame format register - ret = i2c_write_table_val(i2c, DATA_FORMAT_REGS, sizeof(DATA_FORMAT_REGS) / sizeof(DATA_FORMAT_REGS[0])); - // set binning - ret = i2c_write_table_val(i2c, binning_regs, sizeof(binning_regs) / sizeof(binning_regs[0])); - return ret; -} - - -int imx219_stream_start(client interface i2c_master_if i2c){ - int ret = 0; - /* set stream on register */ - ret = i2c_write_table_val(i2c, start_regs, sizeof(start_regs) / sizeof(start_regs[0])); - return ret; -} - -int imx219_stream_stop(client interface i2c_master_if i2c){ - return i2c_write_table(i2c, stop, sizeof(stop) / sizeof(stop[0])); -} - -int imx219_set_gain_dB(client interface i2c_master_if i2c, - uint32_t dBGain) -{ - uint32_t time, again, dgain; - if (dBGain > GAIN_MAX_DB) - { - dBGain = GAIN_MAX_DB; - } - if (dBGain < INTEGRATION_TIMES) - { - time = gain_integration_times[dBGain]; - again = gain_analogue_gains[0]; - dgain = gain_digital_gains[0]; - } - else - { - time = gain_integration_times[INTEGRATION_TIMES - 1]; - if (dBGain < INTEGRATION_TIMES + ANALOGUE_GAINS) - { - again = gain_analogue_gains[dBGain - INTEGRATION_TIMES + 1]; - dgain = gain_digital_gains[0]; - } - else - { - again = gain_analogue_gains[ANALOGUE_GAINS]; - dgain = gain_digital_gains[dBGain - INTEGRATION_TIMES - ANALOGUE_GAINS + 1]; - } - } - int ret = i2c_write(i2c, 0x0157, again); - ret |= i2c_write(i2c, 0x0158, dgain >> 8); - ret |= i2c_write(i2c, 0x0159, dgain & 0xff); - ret |= i2c_write(i2c, 0x015A, time >> 8); - ret |= i2c_write(i2c, 0x015B, time & 0xff); - - return ret; -} +#include +#include +#include + +#include +#include "i2c.h" +#include "imx219.h" +#include "imx219_reg.h" + +#define GAIN_DB 40 + +static int i2c_write(client interface i2c_master_if i2c, int reg, int value) +{ + i2c_regop_res_t result; + // Write an 8-bit register on a slave device from a 16-bit register address. + result = i2c.write_reg8_addr16(IMX219_I2C_ADDR, reg, value); + if (result != I2C_REGOP_SUCCESS) + { + // printf("Failed on address %02x value %02x\n", reg, value); + // TODO FIXME + } + return result != I2C_REGOP_SUCCESS ? -1 : 0; +} + +static int i2c_write_table(client interface i2c_master_if i2c, + imx219_settings_t table[], + int N) { + int ret; + for(int i = 0; i < N; i++) { + uint32_t address = table[i].addr; + uint32_t value = table[i].val; + if (address == SLEEP) { + timer tmr; + int t; + tmr :> t; + tmr when timerafter(t + TRSTUS * 100) :> void; + } + if (address & 0x8000) { + address &= 0x7fff; + ret = i2c_write(i2c, address, value >> 8); // B1 B2 B3 B4 -> B1 B2 + ret |= i2c_write(i2c, address+1, value & 0xff); // B1 B2 B3 B4 -> B3 B4 + } else { + ret = i2c_write(i2c, address, value); + } + if (ret < 0) { + return ret; + } + } + return 0; +} + + +static int i2c_write_table_val(client interface i2c_master_if i2c, + imx219_settings_t table[], + int N) { + int ret; + char mode = 's'; + + for(int i = 0; i < N; i++) { + uint32_t address = table[i].addr; + uint32_t value = table[i].val; + if (address == SLEEP) { + timer tmr; + int t; + tmr :> t; + tmr when timerafter(t + TRSTUS * 100) :> void; + } + + if ((value & 0xFF00) || (address & 0x8000)){ + if (address & 0x8000) { + address &= 0x7fff; + } + mode = 'c'; + //printf("mode=%c , address = 0x%04x, value = 0x%02x\n", mode, address, value >> 8); + //printf("mode=%c , address+ = 0x%04x, value = 0x%02x\n", mode, address+1, value & 0xff); + // continous writte + ret = i2c_write(i2c, address, value >> 8); // B1 B2 B3 B4 -> B1 B2 + ret |= i2c_write(i2c, address+1, value & 0xff); // B1 B2 B3 B4 -> B3 B4 + } + else + { + // single writte + mode = 's'; + ret = i2c_write(i2c, address, value); + //printf("mode=%c , address = 0x%04x, value = 0x%02x\n", mode, address, value); + } + if (ret < 0) { + return ret; + } + } + return 0; +} + + +int imx219_read(client interface i2c_master_if i2c, uint16_t addr){ + i2c_regop_res_t res; + uint16_t val = i2c.read_reg16(IMX219_I2C_ADDR, addr, res); + assert(res == 0); + return val; +} + +void imx219_read_gains(client interface i2c_master_if i2c, uint16_t values[5]){ + values[0] = imx219_read(i2c, 0x0157); + values[1] = imx219_read(i2c, 0x0158); + values[2] = imx219_read(i2c, 0x0159); + values[3] = imx219_read(i2c, 0x015A); + values[4] = imx219_read(i2c, 0x015B); +} + +/// ------------------------------------------------------------------------------- +int imx219_init(client interface i2c_master_if i2c) +{ + int ret = 0; + // Send all registers that are common to all modes + ret = i2c_write_table_val(i2c, imx219_common_regs, sizeof(imx219_common_regs) / sizeof(imx219_common_regs[0])); + // Configure two or four Lane mode + ret = i2c_write_table_val(i2c, imx219_lanes_regs, sizeof(imx219_lanes_regs) / sizeof(imx219_lanes_regs[0])); + // set gain + ret = imx219_set_gain_dB(i2c, GAIN_DB); + return ret; +} + +int imx219_configure_mode(client interface i2c_master_if i2c) +{ + int ret = 0; + // Apply default values of current mode + ret = i2c_write_table_val(i2c, CONFIG_REG, sizeof(CONFIG_REG) / sizeof(CONFIG_REG[0])); + // set frame format register + ret = i2c_write_table_val(i2c, DATA_FORMAT_REGS, sizeof(DATA_FORMAT_REGS) / sizeof(DATA_FORMAT_REGS[0])); + // set binning + ret = i2c_write_table_val(i2c, binning_regs, sizeof(binning_regs) / sizeof(binning_regs[0])); + return ret; +} + + +int imx219_stream_start(client interface i2c_master_if i2c){ + int ret = 0; + /* set stream on register */ + ret = i2c_write_table_val(i2c, start_regs, sizeof(start_regs) / sizeof(start_regs[0])); + return ret; +} + +int imx219_stream_stop(client interface i2c_master_if i2c){ + return i2c_write_table(i2c, stop, sizeof(stop) / sizeof(stop[0])); +} + +int imx219_set_gain_dB(client interface i2c_master_if i2c, + uint32_t dBGain) +{ + uint32_t time, again, dgain; + if (dBGain > GAIN_MAX_DB) + { + dBGain = GAIN_MAX_DB; + } + if (dBGain < INTEGRATION_TIMES) + { + time = gain_integration_times[dBGain]; + again = gain_analogue_gains[0]; + dgain = gain_digital_gains[0]; + } + else + { + time = gain_integration_times[INTEGRATION_TIMES - 1]; + if (dBGain < INTEGRATION_TIMES + ANALOGUE_GAINS) + { + again = gain_analogue_gains[dBGain - INTEGRATION_TIMES + 1]; + dgain = gain_digital_gains[0]; + } + else + { + again = gain_analogue_gains[ANALOGUE_GAINS]; + dgain = gain_digital_gains[dBGain - INTEGRATION_TIMES - ANALOGUE_GAINS + 1]; + } + } + int ret = i2c_write(i2c, 0x0157, again); + ret |= i2c_write(i2c, 0x0158, dgain >> 8); + ret |= i2c_write(i2c, 0x0159, dgain & 0xff); + ret |= i2c_write(i2c, 0x015A, time >> 8); + ret |= i2c_write(i2c, 0x015B, time & 0xff); + + return ret; +} diff --git a/sensors/imx219_reg.h b/sensors/sony_imx219/imx219_reg.h similarity index 95% rename from sensors/imx219_reg.h rename to sensors/sony_imx219/imx219_reg.h index b48ba295..bbe9bb5c 100644 --- a/sensors/imx219_reg.h +++ b/sensors/sony_imx219/imx219_reg.h @@ -1,326 +1,326 @@ - -// --------- REG definitions --------------------------------------------------------- - -// Sleep values and registers -#define SLEEP 0xFFFF -#define TRSTUS 200 - -// CSI LANE -#define CSI_LANE_MODE_REG 0x0114 -#define CSI_LANE_MODE_2_LANES 1 -#define CSI_LANE_MODE_4_LANES 3 - -// BINNING -#define BINNING_MODE_REG 0x0174 -#define BINNING_NONE 0x0000 -#define BINNING_2X2 0x0101 - -#define BINNING_MODE BINNING_2X2 - -// PLL settings -#define PREPLLCK_VT_DIV_REG 0x0304 -#define PREPLLCK_OP_DIV 0x0305 -#define PREDVIDE_2 0x02 -#define PLL_VT_MPY_REG 0x0306 -#define PLL_OP_MPY 0x0010 // no effect in timing performance - -// Gain params -#define GAIN_MIN_DB 0 -#define GAIN_MAX_DB 84 -#define GAIN_DEFAULT_DB 50 - -// --------- REG GROUP definitions ---------------------------------------------------- -static imx219_settings_t imx219_common_regs[] = { - {0x0103, 0x01}, /* software_reset 1, reset the chip */ - {SLEEP, TRSTUS}, /* software_reset 1, reset the chip */ - - - {0x0100, 0x00}, /* Mode Select */ - - /* To Access Addresses 3000-5fff, send the following commands */ - {0x30eb, 0x0c}, - {0x30eb, 0x05}, - {0x300a, 0xff}, - {0x300b, 0xff}, - {0x30eb, 0x05}, - {0x30eb, 0x09}, - - /* PLL Clock Table */ - { 0x812A, 0x1800 }, /* EXCK_FREQ 24.00, for 24 Mhz */ - { 0x0304, 0x02 }, /* PREPLLCK_VT_DIV 2, for pre divide by 2 */ - { 0x0305, 0x02 }, /* PREPLLCK_OP_DIV 2, for pre divide by 2 */ - { 0x8306, PLL_VT_MPY}, /* PLL_VT_MPY 0x27, for multiply by 39, pixclk=187.2 MHz */ - { 0x830C, PLL_OP_MPY}, /* PLL_OP_MPY 0x40, for multiply by 64, MIPI clk=768 MHz */ - { 0x0301, 0x0A }, /* VTPXCK_DIV 5, ? */ - { 0x0303, 0x01 }, /* VTSYCK_DIV 1, ? */ - { 0x0309, 0x0A }, /* OPPXCK_DIV 8, has to match RAW8 if you have raw8*/ - { 0x030B, 0x01 }, /* OPSYCK_DIV 1, has to be 1? */ - - // pck clock - {0x1148, 0x00}, - {0x1149, 0xF0}, - - - /* Undocumented registers */ - {0x455e, 0x00}, - {0x471e, 0x4b}, - {0x4767, 0x0f}, - {0x4750, 0x14}, - {0x4540, 0x00}, - {0x47b4, 0x14}, - {0x4713, 0x30}, - {0x478b, 0x10}, - {0x478f, 0x10}, - {0x4793, 0x10}, - {0x4797, 0x0e}, - {0x479b, 0x0e}, - - /* Frame Bank Register Group "A" */ - {0x0162, 0x0d}, /* Line_Length_A */ - {0x0163, 0x78}, - {0x0170, 0x01}, /* X_ODD_INC_A */ - {0x0171, 0x01}, /* Y_ODD_INC_A */ - - /* Output setup registers */ - {0x0114, 0x01}, /* CSI 2-Lane Mode */ - {0x0128, 0x00}, /* DPHY Auto Mode */ - {0x012a, 0x18}, /* EXCK_Freq */ - {0x012b, 0x00}, -}; - - - - -static imx219_settings_t imx219_lanes_regs[] = { - {CSI_LANE_MODE_REG, CSI_LANE_MODE_2_LANES} -}; - - -#if (CONFIG_MODE == MODE_VGA_640x480) - static imx219_settings_t mode_640_480_regs[] = { - {0x0164, 0x03}, - {0x0165, 0xe8}, - {0x0166, 0x08}, - {0x0167, 0xe7}, - {0x0168, 0x02}, - {0x0169, 0xf0}, - {0x016a, 0x06}, - {0x016b, 0xaf}, - {0x016c, 0x02}, - {0x016d, 0x80}, - {0x016e, 0x01}, - {0x016f, 0xe0}, - {0x0624, 0x06}, - {0x0625, 0x68}, - {0x0626, 0x04}, - {0x0627, 0xd0}, - }; -#endif - -#if (CONFIG_MODE == MODE_UXGA_1640x1232) - static imx219_settings_t mode_1640_1232_regs[] = { - {0x0164, 0x00}, - {0x0165, 0x00}, - {0x0166, 0x0c}, - {0x0167, 0xcf}, - {0x0168, 0x00}, - {0x0169, 0x00}, - {0x016a, 0x09}, - {0x016b, 0x9f}, - {0x016c, 0x06}, - {0x016d, 0x68}, - {0x016e, 0x04}, - {0x016f, 0xd0}, - {0x0624, 0x06}, - {0x0625, 0x68}, - {0x0626, 0x04}, - {0x0627, 0xd0}, - }; -#endif - - -#if (CONFIG_MODE == MODE_FHD_1920x1080) - static imx219_settings_t mode_1920_1080_regs[] = { - {0x0164, 0x02}, - {0x0165, 0xa8}, - {0x0166, 0x0a}, - {0x0167, 0x27}, - {0x0168, 0x02}, - {0x0169, 0xb4}, - {0x016a, 0x06}, - {0x016b, 0xeb}, - {0x016c, 0x07}, - {0x016d, 0x80}, - {0x016e, 0x04}, - {0x016f, 0x38}, - {0x0624, 0x07}, - {0x0625, 0x80}, - {0x0626, 0x04}, - {0x0627, 0x38}, - }; -#endif - - -#if (CONFIG_MODE == MODE_WQSXGA_3280x2464) - static imx219_settings_t mode_3280x2464_regs[] = { - {0x0164, 0x00}, - {0x0165, 0x00}, - {0x0166, 0x0c}, - {0x0167, 0xcf}, - {0x0168, 0x00}, - {0x0169, 0x00}, - {0x016a, 0x09}, - {0x016b, 0x9f}, - {0x016c, 0x0c}, - {0x016d, 0xd0}, - {0x016e, 0x09}, - {0x016f, 0xa0}, - {0x0624, 0x0c}, - {0x0625, 0xd0}, - {0x0626, 0x09}, - {0x0627, 0xa0}, - }; -#endif - -#if (CONFIG_MIPI_FORMAT == MIPI_DT_RAW10) - static imx219_settings_t raw10_framefmt_regs[] = { - {0x018c, 0x0a}, - {0x018d, 0x0a}, - {0x0309, 0x0a}, - }; -#elif (CONFIG_MIPI_FORMAT == MIPI_DT_RAW8) - static imx219_settings_t raw8_framefmt_regs[] = { - {0x018c, 0x08}, - {0x018d, 0x08}, - {0x0309, 0x08}, - }; -#else - #error CONFIG_MIPI_FORMAT not supported -#endif - - -static imx219_settings_t binning_regs[] = { - {BINNING_MODE_REG, BINNING_MODE} -}; - - -static imx219_settings_t start[] = { - {0x0100, 0x01}, /* mode select streaming on */ -}; - -static imx219_settings_t stop[] = { - {0x0100, 0x00}, /* mode select streaming off */ -}; - -static imx219_settings_t start_regs[] = { - {0x0100, 0x01}, /* mode select streaming on */ -}; - -static imx219_settings_t stop_regs[] = { - {0x0100, 0x00}, /* mode select streaming off */ -}; - - - -// GAIN related settings -#define INTEGRATION_TIMES 41 -#define ANALOGUE_GAINS 20 -#define DIGITAL_GAINS 25 - -static uint16_t gain_integration_times[INTEGRATION_TIMES] = { - 0x00a, - 0x00b, - 0x00c, - 0x00e, - 0x010, - 0x012, - 0x014, - 0x016, - 0x019, - 0x01c, - 0x020, - 0x024, - 0x028, - 0x02d, - 0x033, - 0x039, - 0x040, - 0x048, - 0x051, - 0x05b, - 0x066, - 0x072, - 0x080, - 0x090, - 0x0a2, - 0x0b6, - 0x0cc, - 0x0e5, - 0x101, - 0x120, - 0x143, - 0x16b, - 0x197, - 0x1c9, - 0x201, - 0x23f, - 0x286, - 0x2d4, - 0x32d, - 0x390, - 0x400, -}; - -static uint8_t gain_analogue_gains[ANALOGUE_GAINS + 1] = { - 0, - 28, - 53, - 75, - 95, - 112, - 128, - 142, - 155, - 166, - 175, - 184, - 192, - 199, - 205, - 211, - 215, - 220, - 224, - 227, - 231, -}; - -static uint16_t gain_digital_gains[DIGITAL_GAINS + 1] = { - 0x0100, - 0x011f, - 0x0142, - 0x0169, - 0x0195, - 0x01c7, - 0x01fe, - 0x023d, - 0x0283, - 0x02d1, - 0x0329, - 0x038c, - 0x03fb, - 0x0477, - 0x0503, - 0x059f, - 0x064f, - 0x0714, - 0x07f1, - 0x08e9, - 0x0a00, - 0x0b38, - 0x0c96, - 0x0e20, - 0x0fd9, -}; - - + +// --------- REG definitions --------------------------------------------------------- + +// Sleep values and registers +#define SLEEP 0xFFFF +#define TRSTUS 200 + +// CSI LANE +#define CSI_LANE_MODE_REG 0x0114 +#define CSI_LANE_MODE_2_LANES 1 +#define CSI_LANE_MODE_4_LANES 3 + +// BINNING +#define BINNING_MODE_REG 0x0174 +#define BINNING_NONE 0x0000 +#define BINNING_2X2 0x0101 + +#define BINNING_MODE BINNING_2X2 + +// PLL settings +#define PREPLLCK_VT_DIV_REG 0x0304 +#define PREPLLCK_OP_DIV 0x0305 +#define PREDVIDE_2 0x02 +#define PLL_VT_MPY_REG 0x0306 +#define PLL_OP_MPY 0x0010 // no effect in timing performance + +// Gain params +#define GAIN_MIN_DB 0 +#define GAIN_MAX_DB 84 +#define GAIN_DEFAULT_DB 50 + +// --------- REG GROUP definitions ---------------------------------------------------- +static imx219_settings_t imx219_common_regs[] = { + {0x0103, 0x01}, /* software_reset 1, reset the chip */ + {SLEEP, TRSTUS}, /* software_reset 1, reset the chip */ + + + {0x0100, 0x00}, /* Mode Select */ + + /* To Access Addresses 3000-5fff, send the following commands */ + {0x30eb, 0x0c}, + {0x30eb, 0x05}, + {0x300a, 0xff}, + {0x300b, 0xff}, + {0x30eb, 0x05}, + {0x30eb, 0x09}, + + /* PLL Clock Table */ + { 0x812A, 0x1800 }, /* EXCK_FREQ 24.00, for 24 Mhz */ + { 0x0304, 0x02 }, /* PREPLLCK_VT_DIV 2, for pre divide by 2 */ + { 0x0305, 0x02 }, /* PREPLLCK_OP_DIV 2, for pre divide by 2 */ + { 0x8306, PLL_VT_MPY}, /* PLL_VT_MPY 0x27, for multiply by 39, pixclk=187.2 MHz */ + { 0x830C, PLL_OP_MPY}, /* PLL_OP_MPY 0x40, for multiply by 64, MIPI clk=768 MHz */ + { 0x0301, 0x0A }, /* VTPXCK_DIV 5, ? */ + { 0x0303, 0x01 }, /* VTSYCK_DIV 1, ? */ + { 0x0309, 0x0A }, /* OPPXCK_DIV 8, has to match RAW8 if you have raw8*/ + { 0x030B, 0x01 }, /* OPSYCK_DIV 1, has to be 1? */ + + // pck clock + {0x1148, 0x00}, + {0x1149, 0xF0}, + + + /* Undocumented registers */ + {0x455e, 0x00}, + {0x471e, 0x4b}, + {0x4767, 0x0f}, + {0x4750, 0x14}, + {0x4540, 0x00}, + {0x47b4, 0x14}, + {0x4713, 0x30}, + {0x478b, 0x10}, + {0x478f, 0x10}, + {0x4793, 0x10}, + {0x4797, 0x0e}, + {0x479b, 0x0e}, + + /* Frame Bank Register Group "A" */ + {0x0162, 0x0d}, /* Line_Length_A */ + {0x0163, 0x78}, + {0x0170, 0x01}, /* X_ODD_INC_A */ + {0x0171, 0x01}, /* Y_ODD_INC_A */ + + /* Output setup registers */ + {0x0114, 0x01}, /* CSI 2-Lane Mode */ + {0x0128, 0x00}, /* DPHY Auto Mode */ + {0x012a, 0x18}, /* EXCK_Freq */ + {0x012b, 0x00}, +}; + + + + +static imx219_settings_t imx219_lanes_regs[] = { + {CSI_LANE_MODE_REG, CSI_LANE_MODE_2_LANES} +}; + + +#if (CONFIG_MODE == MODE_VGA_640x480) + static imx219_settings_t mode_640_480_regs[] = { + {0x0164, 0x03}, + {0x0165, 0xe8}, + {0x0166, 0x08}, + {0x0167, 0xe7}, + {0x0168, 0x02}, + {0x0169, 0xf0}, + {0x016a, 0x06}, + {0x016b, 0xaf}, + {0x016c, 0x02}, + {0x016d, 0x80}, + {0x016e, 0x01}, + {0x016f, 0xe0}, + {0x0624, 0x06}, + {0x0625, 0x68}, + {0x0626, 0x04}, + {0x0627, 0xd0}, + }; +#endif + +#if (CONFIG_MODE == MODE_UXGA_1640x1232) + static imx219_settings_t mode_1640_1232_regs[] = { + {0x0164, 0x00}, + {0x0165, 0x00}, + {0x0166, 0x0c}, + {0x0167, 0xcf}, + {0x0168, 0x00}, + {0x0169, 0x00}, + {0x016a, 0x09}, + {0x016b, 0x9f}, + {0x016c, 0x06}, + {0x016d, 0x68}, + {0x016e, 0x04}, + {0x016f, 0xd0}, + {0x0624, 0x06}, + {0x0625, 0x68}, + {0x0626, 0x04}, + {0x0627, 0xd0}, + }; +#endif + + +#if (CONFIG_MODE == MODE_FHD_1920x1080) + static imx219_settings_t mode_1920_1080_regs[] = { + {0x0164, 0x02}, + {0x0165, 0xa8}, + {0x0166, 0x0a}, + {0x0167, 0x27}, + {0x0168, 0x02}, + {0x0169, 0xb4}, + {0x016a, 0x06}, + {0x016b, 0xeb}, + {0x016c, 0x07}, + {0x016d, 0x80}, + {0x016e, 0x04}, + {0x016f, 0x38}, + {0x0624, 0x07}, + {0x0625, 0x80}, + {0x0626, 0x04}, + {0x0627, 0x38}, + }; +#endif + + +#if (CONFIG_MODE == MODE_WQSXGA_3280x2464) + static imx219_settings_t mode_3280x2464_regs[] = { + {0x0164, 0x00}, + {0x0165, 0x00}, + {0x0166, 0x0c}, + {0x0167, 0xcf}, + {0x0168, 0x00}, + {0x0169, 0x00}, + {0x016a, 0x09}, + {0x016b, 0x9f}, + {0x016c, 0x0c}, + {0x016d, 0xd0}, + {0x016e, 0x09}, + {0x016f, 0xa0}, + {0x0624, 0x0c}, + {0x0625, 0xd0}, + {0x0626, 0x09}, + {0x0627, 0xa0}, + }; +#endif + +#if (CONFIG_MIPI_FORMAT == MIPI_DT_RAW10) + static imx219_settings_t raw10_framefmt_regs[] = { + {0x018c, 0x0a}, + {0x018d, 0x0a}, + {0x0309, 0x0a}, + }; +#elif (CONFIG_MIPI_FORMAT == MIPI_DT_RAW8) + static imx219_settings_t raw8_framefmt_regs[] = { + {0x018c, 0x08}, + {0x018d, 0x08}, + {0x0309, 0x08}, + }; +#else + #error CONFIG_MIPI_FORMAT not supported +#endif + + +static imx219_settings_t binning_regs[] = { + {BINNING_MODE_REG, BINNING_MODE} +}; + + +static imx219_settings_t start[] = { + {0x0100, 0x01}, /* mode select streaming on */ +}; + +static imx219_settings_t stop[] = { + {0x0100, 0x00}, /* mode select streaming off */ +}; + +static imx219_settings_t start_regs[] = { + {0x0100, 0x01}, /* mode select streaming on */ +}; + +static imx219_settings_t stop_regs[] = { + {0x0100, 0x00}, /* mode select streaming off */ +}; + + + +// GAIN related settings +#define INTEGRATION_TIMES 41 +#define ANALOGUE_GAINS 20 +#define DIGITAL_GAINS 25 + +static uint16_t gain_integration_times[INTEGRATION_TIMES] = { + 0x00a, + 0x00b, + 0x00c, + 0x00e, + 0x010, + 0x012, + 0x014, + 0x016, + 0x019, + 0x01c, + 0x020, + 0x024, + 0x028, + 0x02d, + 0x033, + 0x039, + 0x040, + 0x048, + 0x051, + 0x05b, + 0x066, + 0x072, + 0x080, + 0x090, + 0x0a2, + 0x0b6, + 0x0cc, + 0x0e5, + 0x101, + 0x120, + 0x143, + 0x16b, + 0x197, + 0x1c9, + 0x201, + 0x23f, + 0x286, + 0x2d4, + 0x32d, + 0x390, + 0x400, +}; + +static uint8_t gain_analogue_gains[ANALOGUE_GAINS + 1] = { + 0, + 28, + 53, + 75, + 95, + 112, + 128, + 142, + 155, + 166, + 175, + 184, + 192, + 199, + 205, + 211, + 215, + 220, + 224, + 227, + 231, +}; + +static uint16_t gain_digital_gains[DIGITAL_GAINS + 1] = { + 0x0100, + 0x011f, + 0x0142, + 0x0169, + 0x0195, + 0x01c7, + 0x01fe, + 0x023d, + 0x0283, + 0x02d1, + 0x0329, + 0x038c, + 0x03fb, + 0x0477, + 0x0503, + 0x059f, + 0x064f, + 0x0714, + 0x07f1, + 0x08e9, + 0x0a00, + 0x0b38, + 0x0c96, + 0x0e20, + 0x0fd9, +}; + + diff --git a/sensors/sony_imx219/readme.rst b/sensors/sony_imx219/readme.rst new file mode 100644 index 00000000..a29bd666 --- /dev/null +++ b/sensors/sony_imx219/readme.rst @@ -0,0 +1,25 @@ +=============== +Sony IMX219 Sensor +=============== + +The Sony IMX219 sensor is a widely used image sensor developed by Sony Corporation. It is commonly employed in camera modules for mobile phones and compact electronic devices. + +Key Features +------------ + +- Optical Format: 1/4-inch +- Resolution: 8.3 megapixels +- Technology: Sony Exmor R back-illuminated +- Image Processing: Advanced algorithms for enhanced image quality +- Application: Raspberry Pi Camera Module (versions V2 and V2.1) + +Description +------------ + +The Sony IMX219 sensor offers a compact form factor combined with impressive performance capabilities. Its 1/4-inch optical format allows for integration into small camera modules without compromising image quality. + +With a resolution of 8.3 megapixels, the IMX219 sensor captures detailed images and videos. The Sony Exmor R back-illuminated technology enhances light sensitivity and reduces noise, resulting in improved low-light performance. + +Additionally, the IMX219 sensor incorporates advanced image processing algorithms, ensuring high-quality output. These algorithms optimize color reproduction, sharpness, and dynamic range, delivering visually pleasing results. + +One notable implementation of the Sony IMX219 sensor is in the Raspberry Pi Camera Module. Various versions of the module, such as V2 and V2.1, utilize the IMX219 sensor due to its compact size, power efficiency, and excellent imaging capabilities. From 7c2b9bfa83dbbf437895a773760b128a7e8e240c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 14 Jun 2023 11:00:41 +0100 Subject: [PATCH 086/306] user_api -> camera_api --- camera/api/camera.h | 4 ++-- camera/api/{user_api.h => camera_api.h} | 0 camera/src/{user_api.c => camera_api.c} | 2 +- camera/src/packet_handler.c | 2 +- examples/take_picture_raw/src/app_raw.c | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename camera/api/{user_api.h => camera_api.h} (100%) rename camera/src/{user_api.c => camera_api.c} (99%) diff --git a/camera/api/camera.h b/camera/api/camera.h index c245f519..2af35763 100644 --- a/camera/api/camera.h +++ b/camera/api/camera.h @@ -20,7 +20,7 @@ #include "image_vfilter.h" #include "packet_handler.h" #include "statistics.h" -#include "user_api.h" +#include "camera_api.h" #include "utils.h" #include "isp.h" @@ -82,4 +82,4 @@ of received pixel data, in order to achieve 4-byte alignment of pixel values. 0 = disabled, 1 = enabled (does not require MIPI_SHIM_DEMUX_EN = 1) -*/ \ No newline at end of file +*/ diff --git a/camera/api/user_api.h b/camera/api/camera_api.h similarity index 100% rename from camera/api/user_api.h rename to camera/api/camera_api.h diff --git a/camera/src/user_api.c b/camera/src/camera_api.c similarity index 99% rename from camera/src/user_api.c rename to camera/src/camera_api.c index b7384712..71420241 100644 --- a/camera/src/user_api.c +++ b/camera/src/camera_api.c @@ -6,7 +6,7 @@ // user #include "mipi.h" #include "utils.h" -#include "user_api.h" +#include "camera_api.h" #define CHAN_RAW 0 diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index d2ce85a0..73fe937c 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -8,7 +8,7 @@ #include "packet_handler.h" #include "image_vfilter.h" #include "image_hfilter.h" -#include "user_api.h" +#include "camera_api.h" #include "utils.h" #include "sensor.h" diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index 4a3f5847..85669742 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -5,7 +5,7 @@ #include // user #include "mipi.h" -#include "user_api.h" +#include "camera_api.h" #include "app_raw.h" #include "io_utils.h" From a900cc10b4836616d09927a5d22f140dee979c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 14 Jun 2023 11:03:01 +0100 Subject: [PATCH 087/306] =?UTF-8?q?camera=20=E2=86=92=20camera=5Fmain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- camera/api/{camera.h => camera_main.h} | 0 camera/api/packet_handler.h | 2 +- camera/src/{camera.xc => camera_main.xc} | 2 +- examples/take_picture_downsample/src/app.h | 2 +- examples/take_picture_downsample/src/main.xc | 2 +- examples/take_picture_raw/src/app_raw.h | 2 +- examples/take_picture_raw/src/main.xc | 2 +- tests/unit_tests/src/test/isp_test.c | 4 +--- tests/unit_tests/src/test/pixel_hfilter_test.c | 2 +- tests/unit_tests/src/test/pixel_vfilter_test.c | 4 ++-- 10 files changed, 10 insertions(+), 12 deletions(-) rename camera/api/{camera.h => camera_main.h} (100%) rename camera/src/{camera.xc => camera_main.xc} (98%) diff --git a/camera/api/camera.h b/camera/api/camera_main.h similarity index 100% rename from camera/api/camera.h rename to camera/api/camera_main.h diff --git a/camera/api/packet_handler.h b/camera/api/packet_handler.h index b8b57f78..0a6d8df9 100644 --- a/camera/api/packet_handler.h +++ b/camera/api/packet_handler.h @@ -6,7 +6,7 @@ #include "xccompat.h" -#include "camera.h" +#include "camera_main.h" #include "isp.h" #ifdef __XC__ diff --git a/camera/src/camera.xc b/camera/src/camera_main.xc similarity index 98% rename from camera/src/camera.xc rename to camera/src/camera_main.xc index faac8c53..72da444f 100644 --- a/camera/src/camera.xc +++ b/camera/src/camera_main.xc @@ -9,7 +9,7 @@ #include #include "i2c.h" -#include "camera.h" +#include "camera_main.h" #include "mipi_defines.h" #include "packet_handler.h" #include "statistics.h" diff --git a/examples/take_picture_downsample/src/app.h b/examples/take_picture_downsample/src/app.h index 7419762c..1ddfd014 100644 --- a/examples/take_picture_downsample/src/app.h +++ b/examples/take_picture_downsample/src/app.h @@ -2,6 +2,6 @@ #include "platform.h" #include "xccompat.h" -#include "camera.h" +#include "camera_main.h" void user_app(); diff --git a/examples/take_picture_downsample/src/main.xc b/examples/take_picture_downsample/src/main.xc index 575a794a..5099168d 100644 --- a/examples/take_picture_downsample/src/main.xc +++ b/examples/take_picture_downsample/src/main.xc @@ -4,7 +4,7 @@ #include #include "i2c.h" -#include "camera.h" +#include "camera_main.h" #include "app.h" // I2C interface ports diff --git a/examples/take_picture_raw/src/app_raw.h b/examples/take_picture_raw/src/app_raw.h index 4a9f6110..3853c730 100644 --- a/examples/take_picture_raw/src/app_raw.h +++ b/examples/take_picture_raw/src/app_raw.h @@ -2,6 +2,6 @@ #include "platform.h" #include "xccompat.h" -#include "camera.h" +#include "camera_main.h" void user_app_raw(); diff --git a/examples/take_picture_raw/src/main.xc b/examples/take_picture_raw/src/main.xc index 143b1b2c..b7c8249b 100644 --- a/examples/take_picture_raw/src/main.xc +++ b/examples/take_picture_raw/src/main.xc @@ -4,7 +4,7 @@ #include #include "i2c.h" -#include "camera.h" +#include "camera_main.h" #include "app_raw.h" // I2C interface ports diff --git a/tests/unit_tests/src/test/isp_test.c b/tests/unit_tests/src/test/isp_test.c index 47d0926e..507fed19 100644 --- a/tests/unit_tests/src/test/isp_test.c +++ b/tests/unit_tests/src/test/isp_test.c @@ -10,7 +10,7 @@ #include "unity_fixture.h" -#include "camera.h" +#include "camera_main.h" TEST_GROUP_RUNNER(isp_tests) { RUN_TEST_CASE(isp_tests, yuv_to_rgb); @@ -96,5 +96,3 @@ TEST(isp_tests, rgb_to_yuv) TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.U, ct_result.U); TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.V, ct_result.V); } - - diff --git a/tests/unit_tests/src/test/pixel_hfilter_test.c b/tests/unit_tests/src/test/pixel_hfilter_test.c index 6c38de34..eeb191f5 100644 --- a/tests/unit_tests/src/test/pixel_hfilter_test.c +++ b/tests/unit_tests/src/test/pixel_hfilter_test.c @@ -10,7 +10,7 @@ #include "unity_fixture.h" -#include "camera.h" +#include "camera_main.h" TEST_GROUP_RUNNER(pixel_hfilter) { RUN_TEST_CASE(pixel_hfilter, pixel_hfilter__basic); diff --git a/tests/unit_tests/src/test/pixel_vfilter_test.c b/tests/unit_tests/src/test/pixel_vfilter_test.c index 0717fe29..6d818db5 100644 --- a/tests/unit_tests/src/test/pixel_vfilter_test.c +++ b/tests/unit_tests/src/test/pixel_vfilter_test.c @@ -10,7 +10,7 @@ #include "unity_fixture.h" -#include "camera.h" +#include "camera_main.h" TEST_GROUP_RUNNER(pixel_vfilter) { RUN_TEST_CASE(pixel_vfilter, pixel_vfilter_acc_init__case0); @@ -590,4 +590,4 @@ TEST(pixel_vfilter, pixel_vfilter_macc__timing) printf("\n\t\t%s", row2_head); for(int k = 0; k < max_blocks; k++) printf("%8u", timing[k]); printf("\n\n"); -} \ No newline at end of file +} From 84cc240086e4552b6c9be6a2adc202cf0899e0d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 14 Jun 2023 11:17:00 +0100 Subject: [PATCH 088/306] camera and memcpy --- camera/api/camera_main.h | 2 +- camera/api/{utils.h => camera_utils.h} | 17 +++--- camera/api/isp.h | 9 +++ camera/src/camera_api.c | 15 ++--- camera/src/camera_utils.c | 26 +++++++++ camera/src/isp.c | 17 ++++++ camera/src/packet_handler.c | 2 +- camera/src/utils.c | 55 ------------------- examples/take_picture_downsample/src/app.c | 6 +- .../unit_tests/src/test/pixel_hfilter_test.c | 1 + 10 files changed, 74 insertions(+), 76 deletions(-) rename camera/api/{utils.h => camera_utils.h} (67%) create mode 100644 camera/src/camera_utils.c delete mode 100644 camera/src/utils.c diff --git a/camera/api/camera_main.h b/camera/api/camera_main.h index 2af35763..b6707e62 100644 --- a/camera/api/camera_main.h +++ b/camera/api/camera_main.h @@ -21,7 +21,7 @@ #include "packet_handler.h" #include "statistics.h" #include "camera_api.h" -#include "utils.h" +#include "camera_utils.h" #include "isp.h" diff --git a/camera/api/utils.h b/camera/api/camera_utils.h similarity index 67% rename from camera/api/utils.h rename to camera/api/camera_utils.h index e7a6ff99..6542a2b1 100644 --- a/camera/api/utils.h +++ b/camera/api/camera_utils.h @@ -11,18 +11,15 @@ extern "C" { #endif -static inline -unsigned measure_time(){ - unsigned y = 0; - asm volatile("gettime %0": "=r"(y)); - return y; -} -void c_memcpy(void *dst, void *src, size_t size); +/** + * Measure the cpu ticks + * + * @return ticks - Number of ticks + */ +unsigned measure_time(); + -void rotate_image( - const char *filename, - uint8_t image[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); /** * Convert an array of int8 to an array of uint8. diff --git a/camera/api/isp.h b/camera/api/isp.h index 4bc9e7bb..0c02fda2 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -55,6 +55,15 @@ void isp_bilinear_resize( void isp_rotate_image(const uint8_t *src, uint8_t *dest, int width, int height); + +/** +* Rotate the image by 90 degrees. This is useful for rotating images that are stored in a 3x3 array of uint8_t +* +* @param filename - Name of the file to rotate +* @param image - Array of uint8_t that is to be +*/ +void rotate_image_90(const char *filename, uint8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); + // -------------------------- COLOR CONVERSION ------------------------------------- // Macro arguments to get color components from packed result in the assembly program #define GET_R(rgb) ((rgb >> 16) & 0xFF) diff --git a/camera/src/camera_api.c b/camera/src/camera_api.c index 71420241..8b9c5b77 100644 --- a/camera/src/camera_api.c +++ b/camera/src/camera_api.c @@ -1,11 +1,12 @@ // std #include +#include // xcore #include #include // user #include "mipi.h" -#include "utils.h" +#include "camera_utils.h" #include "camera_api.h" @@ -34,7 +35,7 @@ void camera_api_new_row_raw( { user_handler: user_pixel_data = (int8_t*) s_chan_in_word(c_user_api[CHAN_RAW].end_a); - c_memcpy(user_pixel_data, (void*) pixel_data, W_RAW); + memcpy(user_pixel_data, (void*) pixel_data, W_RAW); s_chan_out_word(c_user_api[CHAN_RAW].end_a, row_index); break; default_handler: @@ -54,7 +55,7 @@ void camera_api_new_row_decimated( { user_handler: user_pixel_data = (int8_t*) s_chan_in_word(c_user_api[CHAN_DEC].end_a); - c_memcpy(user_pixel_data, (void*) pixel_data, CH*W); + memcpy(user_pixel_data, (void*) pixel_data, CH*W); s_chan_out_word(c_user_api[CHAN_DEC].end_a, row_index); break; default_handler: @@ -114,7 +115,7 @@ unsigned camera_capture_image( } while (row_index != 0); for(int c = 0; c < CH; c++) - c_memcpy(&image_buff[c][0][0], &pixel_data[c][0], W); + memcpy(&image_buff[c][0][0], &pixel_data[c][0], W); // Now capture the rest of the rows for (unsigned row = 1; row < H; row++) { @@ -123,7 +124,7 @@ unsigned camera_capture_image( if (row_index != row) return 1; // TODO handle errors better for(int c = 0; c < CH; c++) - c_memcpy(&image_buff[c][row][0], &pixel_data[c][0], W); + memcpy(&image_buff[c][row][0], &pixel_data[c][0], W); } @@ -155,7 +156,7 @@ unsigned camera_capture_image_cropped( } while (row_index != CROP_ROW); for(int c = 0; c < CH; c++) - c_memcpy(&image[c][0][0], &pixel_data[c][CROP_COL], CROP_W); + memcpy(&image[c][0][0], &pixel_data[c][CROP_COL], CROP_W); // Now capture the rest of the rows for (unsigned row = 1; row < CROP_H; row++) { @@ -165,7 +166,7 @@ unsigned camera_capture_image_cropped( if (row_index != row + crop_params.origin.row) return 1; for(int c = 0; c < CH; c++) - c_memcpy(&image[c][row][0], &pixel_data[c][CROP_COL], CROP_W); + memcpy(&image[c][row][0], &pixel_data[c][CROP_COL], CROP_W); } diff --git a/camera/src/camera_utils.c b/camera/src/camera_utils.c new file mode 100644 index 00000000..3f21f029 --- /dev/null +++ b/camera/src/camera_utils.c @@ -0,0 +1,26 @@ + +#include +#include +#include +#include +#include + +#include "camera_utils.h" + + +inline unsigned measure_time() +{ + unsigned y = 0; + asm volatile("gettime %0" + : "=r"(y)); + return y; +} + +void vect_int8_to_uint8( + uint8_t output[], + int8_t input[], + const unsigned length) +{ + for (int k = 0; k < length; k++) + output[k] = input[k] + 128; +} diff --git a/camera/src/isp.c b/camera/src/isp.c index b75b2516..a19b75b9 100644 --- a/camera/src/isp.c +++ b/camera/src/isp.c @@ -264,3 +264,20 @@ void isp_rotate_image(const uint8_t* src, uint8_t* dest, int width, int height) } } } + + +void rotate_image_90( + const char* filename, + uint8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]) +{ + for(int c = 0; c < APP_IMAGE_CHANNEL_COUNT; c++) { + for(int k = 0; k < APP_IMAGE_HEIGHT_PIXELS/2; k++) { + for(int j = 0; j < APP_IMAGE_WIDTH_PIXELS; j++) { + uint8_t a = image_buffer[c][k][j]; + uint8_t b = image_buffer[c][APP_IMAGE_HEIGHT_PIXELS-k-1][APP_IMAGE_WIDTH_PIXELS-j-1]; + image_buffer[c][k][j] = b; + image_buffer[c][APP_IMAGE_HEIGHT_PIXELS-k-1][APP_IMAGE_WIDTH_PIXELS-j-1] = a; + } + } + } +} diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index 73fe937c..9f8ba577 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -9,7 +9,7 @@ #include "image_vfilter.h" #include "image_hfilter.h" #include "camera_api.h" -#include "utils.h" +#include "camera_utils.h" #include "sensor.h" diff --git a/camera/src/utils.c b/camera/src/utils.c deleted file mode 100644 index fde01a75..00000000 --- a/camera/src/utils.c +++ /dev/null @@ -1,55 +0,0 @@ - -#include -#include -#include -#include -#include - -#include "utils.h" - - -// This is called when want to memcpy from Xc to C -void c_memcpy( - void* dst, - void* src, - size_t size) -{ - memcpy(dst, src, size); -} - - -/** -* Rotate the image by 90 degrees. This is useful for rotating images that are stored in a 3x3 array of uint8_t -* -* @param filename - Name of the file to rotate -* @param image - Array of uint8_t that is to be -*/ -void rotate_image( - const char* filename, - uint8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]) -{ - for(int c = 0; c < APP_IMAGE_CHANNEL_COUNT; c++) { - for(int k = 0; k < APP_IMAGE_HEIGHT_PIXELS/2; k++) { - for(int j = 0; j < APP_IMAGE_WIDTH_PIXELS; j++) { - uint8_t a = image_buffer[c][k][j]; - uint8_t b = image_buffer[c][APP_IMAGE_HEIGHT_PIXELS-k-1][APP_IMAGE_WIDTH_PIXELS-j-1]; - image_buffer[c][k][j] = b; - image_buffer[c][APP_IMAGE_HEIGHT_PIXELS-k-1][APP_IMAGE_WIDTH_PIXELS-j-1] = a; - } - } - } -} - -/** - * Convert an array of int8 to an array of uint8. - * - * Data can be updated in-place. - */ -void vect_int8_to_uint8( - uint8_t output[], - int8_t input[], - const unsigned length) -{ - for(int k = 0; k < length; k++) - output[k] = input[k] + 128; -} \ No newline at end of file diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index 2a51769b..80d1591e 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -1,9 +1,11 @@ -#include "app.h" - +#include #include #include + #include "io_utils.h" +#include "app.h" + void user_app() { diff --git a/tests/unit_tests/src/test/pixel_hfilter_test.c b/tests/unit_tests/src/test/pixel_hfilter_test.c index eeb191f5..08e02287 100644 --- a/tests/unit_tests/src/test/pixel_hfilter_test.c +++ b/tests/unit_tests/src/test/pixel_hfilter_test.c @@ -11,6 +11,7 @@ #include "unity_fixture.h" #include "camera_main.h" +#include "camera_utils.h" TEST_GROUP_RUNNER(pixel_hfilter) { RUN_TEST_CASE(pixel_hfilter, pixel_hfilter__basic); From 03ca4a279a3c27ec6a3ac418bd05bc43ca87176a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 14 Jun 2023 11:21:16 +0100 Subject: [PATCH 089/306] sensor changes --- sensors/CMakeLists.txt | 8 ++++---- .../{galaxycore_gc2145 => _galaxycore_gc2145}/gc2145.h | 0 .../{galaxycore_gc2145 => _galaxycore_gc2145}/gc2145.xc | 0 .../gc2145_setup.xc | 0 .../{galaxycore_gc2145 => _galaxycore_gc2145}/readme.rst | 0 sensors/{sony_imx219 => _sony_imx219}/imx219.h | 0 sensors/{sony_imx219 => _sony_imx219}/imx219.xc | 0 sensors/{sony_imx219 => _sony_imx219}/imx219_reg.h | 0 sensors/{sony_imx219 => _sony_imx219}/readme.rst | 0 sensors/{_api => api}/sensor.h | 0 sensors/{_api => api}/sensor_control.h | 0 sensors/{_api => api}/sensor_defs.h | 0 sensors/{_src => src}/sensor_control.xc | 0 13 files changed, 4 insertions(+), 4 deletions(-) rename sensors/{galaxycore_gc2145 => _galaxycore_gc2145}/gc2145.h (100%) rename sensors/{galaxycore_gc2145 => _galaxycore_gc2145}/gc2145.xc (100%) rename sensors/{galaxycore_gc2145 => _galaxycore_gc2145}/gc2145_setup.xc (100%) rename sensors/{galaxycore_gc2145 => _galaxycore_gc2145}/readme.rst (100%) rename sensors/{sony_imx219 => _sony_imx219}/imx219.h (100%) rename sensors/{sony_imx219 => _sony_imx219}/imx219.xc (100%) rename sensors/{sony_imx219 => _sony_imx219}/imx219_reg.h (100%) rename sensors/{sony_imx219 => _sony_imx219}/readme.rst (100%) rename sensors/{_api => api}/sensor.h (100%) rename sensors/{_api => api}/sensor_control.h (100%) rename sensors/{_api => api}/sensor_defs.h (100%) rename sensors/{_src => src}/sensor_control.xc (100%) diff --git a/sensors/CMakeLists.txt b/sensors/CMakeLists.txt index ed3d0322..f3daa8bc 100644 --- a/sensors/CMakeLists.txt +++ b/sensors/CMakeLists.txt @@ -19,10 +19,10 @@ add_library(${LIB_NAME} STATIC) target_include_directories(${LIB_NAME} PUBLIC - _api - _src - galaxycore_gc2145 - sony_imx219 + api + src + _galaxycore_gc2145 + _sony_imx219 ) target_sources(${LIB_NAME} diff --git a/sensors/galaxycore_gc2145/gc2145.h b/sensors/_galaxycore_gc2145/gc2145.h similarity index 100% rename from sensors/galaxycore_gc2145/gc2145.h rename to sensors/_galaxycore_gc2145/gc2145.h diff --git a/sensors/galaxycore_gc2145/gc2145.xc b/sensors/_galaxycore_gc2145/gc2145.xc similarity index 100% rename from sensors/galaxycore_gc2145/gc2145.xc rename to sensors/_galaxycore_gc2145/gc2145.xc diff --git a/sensors/galaxycore_gc2145/gc2145_setup.xc b/sensors/_galaxycore_gc2145/gc2145_setup.xc similarity index 100% rename from sensors/galaxycore_gc2145/gc2145_setup.xc rename to sensors/_galaxycore_gc2145/gc2145_setup.xc diff --git a/sensors/galaxycore_gc2145/readme.rst b/sensors/_galaxycore_gc2145/readme.rst similarity index 100% rename from sensors/galaxycore_gc2145/readme.rst rename to sensors/_galaxycore_gc2145/readme.rst diff --git a/sensors/sony_imx219/imx219.h b/sensors/_sony_imx219/imx219.h similarity index 100% rename from sensors/sony_imx219/imx219.h rename to sensors/_sony_imx219/imx219.h diff --git a/sensors/sony_imx219/imx219.xc b/sensors/_sony_imx219/imx219.xc similarity index 100% rename from sensors/sony_imx219/imx219.xc rename to sensors/_sony_imx219/imx219.xc diff --git a/sensors/sony_imx219/imx219_reg.h b/sensors/_sony_imx219/imx219_reg.h similarity index 100% rename from sensors/sony_imx219/imx219_reg.h rename to sensors/_sony_imx219/imx219_reg.h diff --git a/sensors/sony_imx219/readme.rst b/sensors/_sony_imx219/readme.rst similarity index 100% rename from sensors/sony_imx219/readme.rst rename to sensors/_sony_imx219/readme.rst diff --git a/sensors/_api/sensor.h b/sensors/api/sensor.h similarity index 100% rename from sensors/_api/sensor.h rename to sensors/api/sensor.h diff --git a/sensors/_api/sensor_control.h b/sensors/api/sensor_control.h similarity index 100% rename from sensors/_api/sensor_control.h rename to sensors/api/sensor_control.h diff --git a/sensors/_api/sensor_defs.h b/sensors/api/sensor_defs.h similarity index 100% rename from sensors/_api/sensor_defs.h rename to sensors/api/sensor_defs.h diff --git a/sensors/_src/sensor_control.xc b/sensors/src/sensor_control.xc similarity index 100% rename from sensors/_src/sensor_control.xc rename to sensors/src/sensor_control.xc From 8143dd501bcf7452c48112503f246a1a95de7d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 14 Jun 2023 12:03:32 +0100 Subject: [PATCH 090/306] adding header doxygen style comments --- camera/api/camera_api.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camera/api/camera_api.h b/camera/api/camera_api.h index 4d06faad..8ad4bf8b 100644 --- a/camera/api/camera_api.h +++ b/camera/api/camera_api.h @@ -115,4 +115,4 @@ unsigned camera_capture_image_cropped( #if defined(__XC__) || defined(__cplusplus) } -#endif \ No newline at end of file +#endif From cc275cb9f41255298db2d7577444e436e5807dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 14 Jun 2023 12:03:40 +0100 Subject: [PATCH 091/306] doxygen doc --- README.md => README.rst | 103 +++++++++++++++++++------------------ camera/api/image_vfilter.h | 91 +++++++++++++++++++------------- camera/api/isp.h | 88 +++++++++++++++++++++++++++++-- camera/api/statistics.h | 15 ++++++ camera/src/image_vfilter.c | 8 +-- camera/src/isp.c | 17 +----- camera/src/statistics.c | 15 ------ examples/README.rst | 1 + launch_cmake.sh | 10 ---- modules/mipi/api/mipi.h | 24 ++++++++- 10 files changed, 233 insertions(+), 139 deletions(-) rename README.md => README.rst (86%) create mode 100644 examples/README.rst delete mode 100755 launch_cmake.sh diff --git a/README.md b/README.rst similarity index 86% rename from README.md rename to README.rst index 527d491d..256b40f5 100644 --- a/README.md +++ b/README.rst @@ -1,50 +1,53 @@ -# Camera framework -This repository serves as a comprehensive software solution for camera manipulation using the XCORE.AI sensor. - -## Repository Structure -- **examples** : examples for taking pictures with the explorer board -- **lib_camera** : useful functions to manipulate images -- **modules** : dependencies folder -- **sensors** : camera sensors and API for controlling any camera sensor -- **python** : python functions to decode RAW8, RAW10 pictures and other utilities to treat images - -## Requirements -- CMAKE -- XMOS tools -- git submodules -- Ninja (Windows) - -## Installation -Some dependent components are included as git submodules. These can be obtained by cloning this repository with the following command: -(make sure you have the correct ssh access to clone) -``` -git clone --recurse-submodules git@github.com:xmos/fwk_camera.git -``` - -## Build -Linux, Mac -``` -sh launch_cmake.sh -cd build/ -make {YOUR_EXAMPLE} -``` -Windows -``` -cmake -G Ninja -B build -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -cd build/ -ninja -``` - -## Useful commands -- run (explorer board): ```xrun --xscope example_take_picture.xe``` -- run (simulation): ```xsim --xscope "-offline trace.xmt" build/example_take_picture.xe``` - -## Supported Cameras - -| Model | Max Resolution | Output Formats -| ------------- | ------------- | ------------- | -| IMX219 | 640Hx480V == VGA (2x2 binning) | RAW8 -| GC2145 | 1600H x 1200V == 2MPX | YUV422 - -## How to configure your sensor or add a new one -TODO +# Camera framework +This repository serves as a comprehensive software solution for camera manipulation using the XCORE.AI sensor. + +## Repository Structure +- **examples** : examples for taking pictures with the explorer board +- **lib_camera** : useful functions to manipulate images +- **modules** : dependencies folder +- **sensors** : camera sensors and API for controlling any camera sensor +- **python** : python functions to decode RAW8, RAW10 pictures and other utilities to treat images + +## Requirements +- CMAKE +- XMOS tools +- git submodules +- Ninja (Windows) + +## Installation +Some dependent components are included as git submodules. These can be obtained by cloning this repository with the following command: +(make sure you have the correct ssh access to clone) +``` +git clone --recurse-submodules git@github.com:xmos/fwk_camera.git +``` + +## Build instructions +The instructions below will build all modules, examples and tests. +For building a cpecific example refere to examples/readme.rst. + +Linux, Mac +``` +cmake -B build -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake +cd build/ +make +``` +Windows +``` +cmake -G Ninja -B build -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake +cd build/ +ninja +``` + +## Useful commands +- run (explorer board): ```xrun --xscope example_take_picture.xe``` +- run (simulation): ```xsim --xscope "-offline trace.xmt" build/example_take_picture.xe``` + +## Supported Cameras + +| Model | Max Resolution | Output Formats +| ------------- | ------------- | ------------- | +| IMX219 | 640Hx480V == VGA (2x2 binning) | RAW8 +| GC2145 | 1600H x 1200V == 2MPX | YUV422 + +## How to configure your sensor or add a new one +TODO diff --git a/camera/api/image_vfilter.h b/camera/api/image_vfilter.h index 1b3924ab..d47a2eb2 100644 --- a/camera/api/image_vfilter.h +++ b/camera/api/image_vfilter.h @@ -2,7 +2,6 @@ #define IMAGE_VFILTER_H #include - #include "sensor.h" @@ -36,117 +35,137 @@ extern "C" { #endif +typedef struct { + int next_tap; + int16_t buff[VFILTER_ACC_WIDTH_SHORTS]; +} vfilter_acc_t; /** * Initialize a vector of 32-bit split accumulators to a given value. - * + * * This function is used to initialize a set of accumulators to an offset value * to prepare for filtering. - * + * * The `pixel_vfilter_*()` functions use the VPU in 8-bit mode. All accumulators * are initialized to the same 32-bit value, with the values split as required * by the VPU. - * + * * Accordingly, `pix_count` must be a multiple of 16. - * + * * @param accs The vector of accumulators to initialize. * @param acc_value The value to initialize the accumulators to. * @param pix_count The number of accumulators to initialize. */ void pixel_vfilter_acc_init( - int16_t* accs, + int16_t *accs, const int32_t acc_value, const unsigned pix_count); /** * Generate output pixel values from a vector of accumulators. - * + * * This function is to be called on a row of accumulators after the final filter * tap has been applied. The 32-bit accumulators are right-shifted by the values * in `shifts[]` (with rounding), saturated to 8-bit symmetric bounds, and * dropped from int32_t to int8_t before being written to `pix_out[]`. - * + * * `pix_out[]`, `accs` and `shifts` must each be aligned to a 4-byte boundary. - * + * * `accs` is expected to be formatted according to the VPU's split 32-bit * accumulator format. - * + * * `shifts[k]` will be applied to accumulators with indices `k mod 16`. Usually, * each `shift[k]` will be the same. - * + * * Due to VPU limitations, `pix_count` must be a multiple of 16. - * + * * `pix_count` bytes will be written to `pix_out[]`. - * + * * @param pix_out The output pixel values. * @param accs The vector of accumulators to generate output from. * @param shifts The right-shifts to apply to each accumulator. * @param pix_count The number of output pixels. */ void pixel_vfilter_complete( - int8_t* pix_out, - const int16_t* accs, + int8_t *pix_out, + const int16_t *accs, const int16_t shifts[16], const unsigned pix_count); - /** * Apply a filter tap to a vector of accumulators using the supplied input * pixels. - * + * * This function is used to apply a single filter tap to a vector of * accumulators (i.e. `pix_count` separate filters are updated). - * + * * If `ACC32[k]` is the 32-bit accumulator associated with output pixel `k`, * then this function computes: - * + * * ACC32[k] += (pix_in[k] * filter[k % 16]) - * + * * Because this function is used for incrementally applying the same vertical * filter across many columns of an image, the values `filter[k]` will typically * all be the same value, `COEF` for a given call to this function, simplifying * to: - * + * * ACC32[k] += (pix_in[k] * COEF) - * + * * The `pix_in[]` array is expected to contain `pix_count` 8-bit pixels. - * + * * `accs` is expected to be formatted according to the VPU's split 32-bit * accumulator format. - * + * * `accs`, `pix_in` and `filter[]` must each be 4-byte aligned. - * + * * Due to VPU limitations, `pix_count` must be a multiple of 16. - * + * * When the final filter tap is applied, `pixel_vfilter_complete()` should be * called to generate output pixels from the accumulators. - * + * * @param accs The vector of accumulators to apply the filter tap to. * @param pix_in The input pixels to apply to the filter. * @param filter Vector containing filter coefficients. * @param pix_count The number of pixels to apply the filter to. */ void pixel_vfilter_macc( - int16_t* accs, - const int8_t* pix_in, + int16_t *accs, + const int8_t *pix_in, const int8_t filter[16], const unsigned pix_count); - - -typedef struct { - int next_tap; - int16_t buff[VFILTER_ACC_WIDTH_SHORTS]; -} vfilter_acc_t; - +/** + * @brief Initialize a vector of vertical filter accumulators. + * + * @param accs The vector of accumulators to initialize. + */ void image_vfilter_frame_init( vfilter_acc_t accs[]); +/** + * @brief Apply a filter tap to a vector of accumulators using the supplied input + * + * @param output output vector after the filtering process + * @param acc vector of accumulators + * @param pixel_data input data + * @return unsigned 1 if rows are finished + */ unsigned image_vfilter_process_row( int8_t output[], vfilter_acc_t acc[], const int8_t pixel_data[]); +/** + * After the last line of the image, some of the accumulators will be midway + * through processing the image but still need to be output without maccing + * any more inputs. + * + * Keep calling this until it returns 0. + * + * @param output output vector after the filtering process + * @param acc array of accumulators + * @return unsigned 0 when finished + */ unsigned image_vfilter_drain( int8_t output[], vfilter_acc_t acc[]); diff --git a/camera/api/isp.h b/camera/api/isp.h index 0c02fda2..59b5139e 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -11,12 +11,47 @@ #define BLACK_LEVEL 16 // ---------------------------------- AE/AGC ------------------------------ + +/** + * @brief auto exposure control funciton + * + * @param global_stats structure containing the global statistics + */ void AE_control_exposure( global_stats_t *global_stats, CLIENT_INTERFACE(sensor_control_if, sc_if)); + +/** + * @brief aux function to print the skewness + * + * @param gstats structure containing the global statistics + */ void AE_print_skewness(global_stats_t *gstats); + +/** + * @brief function to compute the mean skewness + * + * @param gstats structure containing the global statistics + * @return float skewness normalized from (-1,1) + */ float AE_compute_mean_skewness(global_stats_t *gstats); + +/** + * @brief checks if the skewness is inside the desired interval + * + * @param sk skewness + * @return uint8_t 1 if the skewness is inside the interval, 0 otherwise + */ uint8_t AE_is_adjusted(float sk); + +/** + * @brief computes the new exposure value + * it uses false position step for computing the exposure + * https://research.ijcaonline.org/volume83/number14/pxc3892895.pdf + * @param exposure + * @param skewness + * @return uint8_t + */ uint8_t AE_compute_new_exposure(float exposure, float skewness); @@ -33,18 +68,50 @@ typedef struct { float channel_gain[APP_IMAGE_CHANNEL_COUNT]; } isp_params_t; - +// current isp parameters for white balancing extern isp_params_t isp_params; + +/** + * @brief auto white balance control function + * + * @param gstats structure containing the global statistics + * @param isp_params structure containing the current isp parameters + */ void AWB_compute_gains(global_stats_t *gstats, isp_params_t *isp_params); + +/** + * @brief aux function to print the auto white balancing gains + * + * @param isp_params structure containing the current isp parameters + */ void AWB_print_gains(isp_params_t *isp_params); // ---------------------------------- GAMMA ------------------------------ +// Gamma correction table extern const uint8_t gamma_1p8_s1[255]; + +/** + * @brief gamma correction function + * + * @param buffsize size of the image buffer + * @param img pointer to the image buffer + */ void isp_gamma_stride1(const uint32_t buffsize, uint8_t *img); // -------------------------- ROTATE/RESIZE ------------------------------------- + +/** + * @brief bilinear interpolation function + * + * @param in_width width of the input image + * @param in_height height of the input image + * @param img pointer to the input image + * @param out_width output width + * @param out_height output height + * @param out_img pointer to the output image + */ void isp_bilinear_resize( const uint16_t in_width, const uint16_t in_height, @@ -53,9 +120,6 @@ void isp_bilinear_resize( const uint16_t out_height, uint8_t *out_img); -void isp_rotate_image(const uint8_t *src, uint8_t *dest, int width, int height); - - /** * Rotate the image by 90 degrees. This is useful for rotating images that are stored in a 3x3 array of uint8_t * @@ -74,11 +138,27 @@ void rotate_image_90(const char *filename, uint8_t image_buffer[APP_IMAGE_CHANNE #define GET_U(yuv) GET_G(yuv) #define GET_V(yuv) GET_B(yuv) +/** + * @brief converts a YUV pixel to RGB + * + * @param y Y component + * @param u U component + * @param v V component + * @return int result of rgb conversion (need macros to decode output) + */ int yuv_to_rgb( int y, int u, int v); +/** + * @brief converts a RGB pixel to YUV + * + * @param r red component + * @param g green component + * @param b blue component + * @return int result of yuv conversion (need macros to decode output) + */ int rgb_to_yuv( int r, int g, diff --git a/camera/api/statistics.h b/camera/api/statistics.h index 2c4b9ee7..db01f309 100644 --- a/camera/api/statistics.h +++ b/camera/api/statistics.h @@ -53,8 +53,23 @@ typedef struct { typedef channel_stats_t global_stats_t[APP_IMAGE_CHANNEL_COUNT]; // Statistics compute funtions + +/** +* Compute skewness of channel. +* This is used by auto exposure +* @param stats - * Pointer to channel statistics to update. +*/ void compute_skewness(channel_stats_t *stats); + +/** +* Compute simple statistics for a set of data. +* @param stats - * Pointer to the channel statistics to be computed +*/ void compute_simple_stats(channel_stats_t *stats); + +/** + * Find the value for which (fraction) portion of pixels fall below that value. + */ void find_percentile(channel_stats_t *stats, const float fraction); diff --git a/camera/src/image_vfilter.c b/camera/src/image_vfilter.c index 912b6505..dba7a345 100644 --- a/camera/src/image_vfilter.c +++ b/camera/src/image_vfilter.c @@ -98,13 +98,7 @@ unsigned image_vfilter_process_row( return 0; } -/** - * After the last line of the image, some of the accumulators will be midway - * through processing the image but still need to be output without maccing - * any more inputs. - * - * Keep calling this until it returns 0. - */ + unsigned image_vfilter_drain( int8_t output[], vfilter_acc_t acc[]) diff --git a/camera/src/isp.c b/camera/src/isp.c index a19b75b9..a2f8d312 100644 --- a/camera/src/isp.c +++ b/camera/src/isp.c @@ -49,7 +49,7 @@ float AE_compute_mean_skewness(global_stats_t *gstats){ return sk; } -uint8_t AE_is_adjusted(float sk) { +inline uint8_t AE_is_adjusted(float sk) { return (sk < AE_MARGIN && sk > -AE_MARGIN) ? 1 : 0; } @@ -251,21 +251,6 @@ void isp_bilinear_resize( } } - -void isp_rotate_image(const uint8_t* src, uint8_t* dest, int width, int height) { - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - // Calculate the new coordinates after rotation - int new_x = height - 1 - y; - int new_y = x; - - // Copy the pixel value to the new position - dest[new_y * height + new_x] = src[y * width + x]; - } - } -} - - void rotate_image_90( const char* filename, uint8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]) diff --git a/camera/src/statistics.c b/camera/src/statistics.c index 2735bea7..cfe6461f 100644 --- a/camera/src/statistics.c +++ b/camera/src/statistics.c @@ -33,12 +33,6 @@ void update_histogram( } - -/** -* Compute skewness of channel. -* This is used by auto exposure -* @param stats - * Pointer to channel statistics to update. -*/ void compute_skewness(channel_stats_t *stats) { const float zk_values[] = { @@ -64,12 +58,6 @@ void compute_skewness(channel_stats_t *stats) } - - -/** -* Compute simple statistics for a set of data. -* @param stats - * Pointer to the channel statistics to be computed -*/ void compute_simple_stats(channel_stats_t *stats) { // Calculate the histogram @@ -99,9 +87,6 @@ void compute_simple_stats(channel_stats_t *stats) } -/** - * Find the value for which (fraction) portion of pixels fall below that value. - */ void find_percentile(channel_stats_t *stats, const float fraction) { const unsigned threshold = fraction * HISTOGRAM_TOTAL_SAMPLES; diff --git a/examples/README.rst b/examples/README.rst new file mode 100644 index 00000000..8ad1dfb5 --- /dev/null +++ b/examples/README.rst @@ -0,0 +1 @@ +//TODO diff --git a/launch_cmake.sh b/launch_cmake.sh deleted file mode 100755 index a211a9d9..00000000 --- a/launch_cmake.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -# clean everything -rm -r build/ - - -# build again -echo "Building everything up" -sleep 3 -cmake -B build -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake \ No newline at end of file diff --git a/modules/mipi/api/mipi.h b/modules/mipi/api/mipi.h index 64d5b3e0..45c293e9 100644 --- a/modules/mipi/api/mipi.h +++ b/modules/mipi/api/mipi.h @@ -21,6 +21,20 @@ typedef port_t in_buffered_port_32_t; typedef port_t in_port_t; #endif + +/** + * @brief Initialize the MIPI packet handler. + * + * @param tile Tile where the MIPI packet handler is running + * @param p_mipi_rxd High-Speed Receive Data + * @param p_mipi_rxv High-Speed Receive Data Valid. This active high signal indicates that the lane module is driving valid data to the protocol on the RxDataHS output. + * @param p_mipi_rxa RxActiveHS (Output): High-Speed Reception Active. This active high signal indicates that the lane module is actively receiving a high-speed transmission from the lane interconnect. + * @param p_mipi_clk High-Speed Receive Byte Clock. This signal is used to synchronize signals in the high-speed receive clock domain. + * @param clk_mipi Clock block used to generate the MIPI clock + * @param mipi_shim_cfg0 Configuration of the mipi shim + * @param mipiClkDiv MIPI clock divider + * @param cfgClkDiv Configuration clock divider + */ void MipiPacketRx_init( tileref_t tile, in_buffered_port_32_t p_mipi_rxd, @@ -32,9 +46,17 @@ void MipiPacketRx_init( uint32_t mipiClkDiv, uint32_t cfgClkDiv); +/** + * @brief Mipi packet reciever + * + * @param p_mipi_rxd High-Speed Receive Data + * @param p_mipi_rxa RxActiveHS (Output): High-Speed Reception Active. This active high signal indicates that the lane module is actively receiving a high-speed transmission from the lane interconnect. + * @param c_pkt Channel to send packets to + * @param c_ctrl Channel to send control messages to + */ void MipiPacketRx( in_buffered_port_32_t p_mipi_rxd, in_port_t p_mipi_rxa, streaming_chanend_t c_pkt, streaming_chanend_t c_ctrl); - \ No newline at end of file + From 93207cc69090bc9f7d4a76698906a6d3d9379bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 14 Jun 2023 12:07:58 +0100 Subject: [PATCH 092/306] rst reformat --- README.rst | 80 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/README.rst b/README.rst index 256b40f5..39c5430e 100644 --- a/README.rst +++ b/README.rst @@ -1,53 +1,77 @@ -# Camera framework +Camera framework +================== + This repository serves as a comprehensive software solution for camera manipulation using the XCORE.AI sensor. -## Repository Structure +Repository Structure +-------------------- + - **examples** : examples for taking pictures with the explorer board - **lib_camera** : useful functions to manipulate images - **modules** : dependencies folder - **sensors** : camera sensors and API for controlling any camera sensor - **python** : python functions to decode RAW8, RAW10 pictures and other utilities to treat images -## Requirements +Requirements +------------ + - CMAKE - XMOS tools - git submodules - Ninja (Windows) -## Installation +Installation +------------ + Some dependent components are included as git submodules. These can be obtained by cloning this repository with the following command: (make sure you have the correct ssh access to clone) -``` -git clone --recurse-submodules git@github.com:xmos/fwk_camera.git -``` -## Build instructions +.. code-block:: bash + + git clone --recurse-submodules git@github.com:xmos/fwk_camera.git + +Build instructions +------------------ + The instructions below will build all modules, examples and tests. -For building a cpecific example refere to examples/readme.rst. +For building a specific example refer to examples/readme.rst. Linux, Mac -``` -cmake -B build -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -cd build/ -make -``` +~~~~~~~~~~ + +.. code-block:: bash + + cmake -B build -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake + cd build/ + make + Windows -``` -cmake -G Ninja -B build -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -cd build/ -ninja -``` +~~~~~~~~ + +.. code-block:: bash + + cmake -G Ninja -B build -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake + cd build/ + ninja + +Useful commands +--------------- + +- run (explorer board): ``xrun --xscope example_take_picture.xe`` +- run (simulation): ``xsim --xscope "-offline trace.xmt" build/example_take_picture.xe`` -## Useful commands -- run (explorer board): ```xrun --xscope example_take_picture.xe``` -- run (simulation): ```xsim --xscope "-offline trace.xmt" build/example_take_picture.xe``` +Supported Cameras +----------------- -## Supported Cameras ++-------------+-----------------+-----------------+ +| Model | Max Resolution | Output Formats | ++=============+=================+=================+ +| IMX219 | 640Hx480V == VGA (2x2 binning) | RAW8 | ++-------------+-----------------+-----------------+ +| GC2145 | 1600H x 1200V == 2MPX | YUV422 | ++-------------+-----------------+-----------------+ -| Model | Max Resolution | Output Formats -| ------------- | ------------- | ------------- | -| IMX219 | 640Hx480V == VGA (2x2 binning) | RAW8 -| GC2145 | 1600H x 1200V == 2MPX | YUV422 +How to configure your sensor or add a new one +-------------------------------------------- -## How to configure your sensor or add a new one TODO From 84baf9227787a0fa4092f8d05c0563e3c8917ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 14 Jun 2023 14:02:55 +0100 Subject: [PATCH 093/306] changing readme.rst --- README.rst | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 39c5430e..85e91cef 100644 --- a/README.rst +++ b/README.rst @@ -54,11 +54,6 @@ Windows cd build/ ninja -Useful commands ---------------- - -- run (explorer board): ``xrun --xscope example_take_picture.xe`` -- run (simulation): ``xsim --xscope "-offline trace.xmt" build/example_take_picture.xe`` Supported Cameras ----------------- @@ -68,6 +63,8 @@ Supported Cameras +=============+=================+=================+ | IMX219 | 640Hx480V == VGA (2x2 binning) | RAW8 | +-------------+-----------------+-----------------+ +| IMX219 | 160Hx120V (2x2 binning), downsampled | RGB | ++-------------+-----------------+-----------------+ | GC2145 | 1600H x 1200V == 2MPX | YUV422 | +-------------+-----------------+-----------------+ From bd3b52fc0b506ce948a85bad4135bcfc55e944c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 14 Jun 2023 14:07:23 +0100 Subject: [PATCH 094/306] rst table --- README.rst | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 85e91cef..70f5aa6b 100644 --- a/README.rst +++ b/README.rst @@ -55,18 +55,19 @@ Windows ninja + Supported Cameras ----------------- -+-------------+-----------------+-----------------+ -| Model | Max Resolution | Output Formats | -+=============+=================+=================+ -| IMX219 | 640Hx480V == VGA (2x2 binning) | RAW8 | -+-------------+-----------------+-----------------+ -| IMX219 | 160Hx120V (2x2 binning), downsampled | RGB | -+-------------+-----------------+-----------------+ -| GC2145 | 1600H x 1200V == 2MPX | YUV422 | -+-------------+-----------------+-----------------+ ++--------+--------------------------------+----------------+ +| Model | Max Resolution | Output Formats | ++========+================================+================+ +| IMX219 | 640Hx480V == VGA (2x2 binning) | RAW8 | ++--------+--------------------------------+----------------+ +| IMX219 | 160x120x3 RGB | RGB | ++--------+--------------------------------+----------------+ +| GC2145 | 1600H x 1200V == 2MPX | YUV422 | ++--------+--------------------------------+----------------+ How to configure your sensor or add a new one -------------------------------------------- From c6fa473c2218a4c220fa47c631ea6a1871213ea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 14 Jun 2023 14:08:42 +0100 Subject: [PATCH 095/306] rst cleanup --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 70f5aa6b..2462d28c 100644 --- a/README.rst +++ b/README.rst @@ -64,7 +64,7 @@ Supported Cameras +========+================================+================+ | IMX219 | 640Hx480V == VGA (2x2 binning) | RAW8 | +--------+--------------------------------+----------------+ -| IMX219 | 160x120x3 RGB | RGB | +| IMX219 | 160x120x3 RGB | RGB | +--------+--------------------------------+----------------+ | GC2145 | 1600H x 1200V == 2MPX | YUV422 | +--------+--------------------------------+----------------+ From 2e22d45a9d765d46e7937189a932759ec62118d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 14 Jun 2023 14:57:40 +0100 Subject: [PATCH 096/306] typo fix --- examples/take_picture_raw/src/app_raw.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index baceeed9..85669742 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -5,10 +5,6 @@ #include // user #include "mipi.h" -<<<<<<< HEAD -//#include "utils.h" -======= ->>>>>>> develop #include "camera_api.h" #include "app_raw.h" #include "io_utils.h" From 1d094c554ecbb77547dd032e379b54496c1699b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Thu, 15 Jun 2023 15:27:29 +0100 Subject: [PATCH 097/306] dynamic awb --- .gitignore | 12 +- .gitmodules | 2 +- camera/api/isp.h | 17 +- camera/api/statistics.h | 7 +- camera/src/isp.c | 174 +++++++++++++++++---- camera/src/statistics.c | 40 +++-- examples/take_picture_downsample/src/app.c | 11 ++ examples/take_picture_raw/src/app_raw.c | 2 +- python/FIR_pipeline.py | 102 +++++++++--- python/compare_images.py | 69 ++++++++ python/decode_raw8.py | 3 +- utils/xscope_fileio | 2 +- 12 files changed, 361 insertions(+), 80 deletions(-) create mode 100644 python/compare_images.py diff --git a/.gitignore b/.gitignore index 1680b94a..5e055b8c 100644 --- a/.gitignore +++ b/.gitignore @@ -83,4 +83,14 @@ temp # dotenv .env -**/*.png \ No newline at end of file +**/*.png + +# ninja +.ninja_deps +.ninja_log +/build.ninja +*.exe + +# Images +*.bmp +*.raw diff --git a/.gitmodules b/.gitmodules index 9fd458d0..80f9451e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,7 +13,7 @@ path = tests/Unity url = git@github.com:xmos/Unity # unit testing branch = develop - [submodule "utils/xscope_fileio"] path = utils/xscope_fileio url = https://github.com/xmos/xscope_fileio + branch = develop diff --git a/camera/api/isp.h b/camera/api/isp.h index 59b5139e..74a2eecf 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -17,7 +17,7 @@ * * @param global_stats structure containing the global statistics */ -void AE_control_exposure( +uint8_t AE_control_exposure( global_stats_t *global_stats, CLIENT_INTERFACE(sensor_control_if, sc_if)); @@ -77,7 +77,12 @@ extern isp_params_t isp_params; * @param gstats structure containing the global statistics * @param isp_params structure containing the current isp parameters */ -void AWB_compute_gains(global_stats_t *gstats, isp_params_t *isp_params); +void AWB_compute_gains_static(global_stats_t *gstats, isp_params_t *isp_params); + +//TODO +void AWB_compute_gains_white_patch(global_stats_t *gstats, isp_params_t *isp_params); + +void AWB_compute_gains_gray_world(global_stats_t *gstats, isp_params_t *isp_params); /** * @brief aux function to print the auto white balancing gains @@ -86,6 +91,13 @@ void AWB_compute_gains(global_stats_t *gstats, isp_params_t *isp_params); */ void AWB_print_gains(isp_params_t *isp_params); +/** + * @brief auto white balance control function based on percentile + * + * @param gstats structure containing the global statistics + * @param percentile percentile to compute + */ +void AWB_compute_gains_percentile(global_stats_t *gstats, isp_params_t *isp_params); // ---------------------------------- GAMMA ------------------------------ // Gamma correction table @@ -99,6 +111,7 @@ extern const uint8_t gamma_1p8_s1[255]; */ void isp_gamma_stride1(const uint32_t buffsize, uint8_t *img); +void isp_gamma_1p8(uint8_t *img_in, const size_t height, const size_t width, const size_t channels); // -------------------------- ROTATE/RESIZE ------------------------------------- diff --git a/camera/api/statistics.h b/camera/api/statistics.h index db01f309..70c7a8dd 100644 --- a/camera/api/statistics.h +++ b/camera/api/statistics.h @@ -5,6 +5,7 @@ #include // memset #include // null #include // free, alloc +#include #include "xccompat.h" @@ -29,7 +30,7 @@ extern "C" { // fraction. (0.95 will find the value which 95% of pixels are less than or // equal to) #ifndef APP_WB_PERCENTILE -#define APP_WB_PERCENTILE (0.94) +#define APP_WB_PERCENTILE (0.98) #endif // Objects definitions @@ -38,7 +39,7 @@ typedef struct { } low_res_image_row_t; typedef struct { - int bins[HISTOGRAM_BIN_COUNT]; + uint32_t bins[HISTOGRAM_BIN_COUNT]; } channel_histogram_t; typedef struct { @@ -67,6 +68,8 @@ void compute_skewness(channel_stats_t *stats); */ void compute_simple_stats(channel_stats_t *stats); +void print_simple_stats(channel_stats_t *stats); + /** * Find the value for which (fraction) portion of pixels fall below that value. */ diff --git a/camera/src/isp.c b/camera/src/isp.c index a2f8d312..539e1ded 100644 --- a/camera/src/isp.c +++ b/camera/src/isp.c @@ -5,12 +5,17 @@ // ---------------------------------- utils ------------------------------ static -uint8_t csign(float x) { +int8_t csign(float x) { return (x > 0) - (x < 0); } +static +float cabs(float x) { + return x * csign(x); +} + // ---------------------------------- AE / AGC ------------------------------ -void AE_control_exposure( +uint8_t AE_control_exposure( global_stats_t *global_stats, CLIENT_INTERFACE(sensor_control_if, sc_if)) { @@ -24,6 +29,7 @@ void AE_control_exposure( if (printf_info){ printf("-----> adjustement done\n"); printf_info = 0; + return 1; } } else{ // Adjust exposure @@ -31,6 +37,7 @@ void AE_control_exposure( sensor_control_set_exposure(sc_if, (uint8_t)new_exp); printf_info = 1; } + return 0; } void AE_print_skewness(global_stats_t *gstats){ @@ -41,12 +48,11 @@ void AE_print_skewness(global_stats_t *gstats){ } float AE_compute_mean_skewness(global_stats_t *gstats){ - float sk = 0.0; - sk += (*gstats)[0].skewness; - sk += (*gstats)[1].skewness; - sk += (*gstats)[2].skewness; - sk = sk / 3.0; - return sk; + float mean = ( + (*gstats)[0].skewness + \ + (*gstats)[1].skewness + \ + (*gstats)[2].skewness)/3; + return mean; } inline uint8_t AE_is_adjusted(float sk) { @@ -89,7 +95,7 @@ uint8_t AE_compute_new_exposure(float exposure, float skewness) // ---------------------------------- AWB ------------------------------ const float AWB_ceil = 255.0; -const float AWB_MAX = 1.6; +const float AWB_MAX = 1.7; const float AWB_MIN = 0.8; isp_params_t isp_params = { @@ -111,12 +117,11 @@ float AWB_clip_value(float tmp){ return tmp; } -static void AWB_compute_gains_percentile(global_stats_t *gstats, isp_params_t *isp_params){ // Adjust AWB - float tmp1=1.3; - float tmp2=1.0; - float tmp3=1.3; + float tmp0=1.3; + float tmp1=0.8; + float tmp2=1.3; // percentile adjustement printf("%d,%d,%d,%d,%d,%d,\n", @@ -127,34 +132,88 @@ void AWB_compute_gains_percentile(global_stats_t *gstats, isp_params_t *isp_para (*gstats)[2].min, (*gstats)[2].max); - tmp1 = 254 / (float)(*gstats)[0].percentile; // RED - tmp2 = 254 / (float)(*gstats)[1].percentile; // GREEN - tmp3 = 254 / (float)(*gstats)[2].percentile; // BLUE + tmp0 = 255.0 / (float)(*gstats)[0].percentile; // RED + tmp1 = 255.0 / (float)(*gstats)[1].percentile; // GREEN + tmp2 = 255.0 / (float)(*gstats)[2].percentile; // BLUE - tmp1 = 0.2*(1.3-tmp1) + 1.3; - tmp2 = 0.2*(1-tmp2) + 1; - tmp3 = 0.2*(1.3-tmp3) + 1.3; + + // add skewness contribution + const float skmin = -1; + const float skmax = 1; + const float gmin = 1; + const float gmax = 1.5; + const float grange = gmax - gmin; + const float skrange = skmax - skmin; + const float m = -(grange/skrange); + + float gains[3]; + + for (int i=0; i< 3; i++){ + float sk = (*gstats)[i].skewness; + gains[i] = m*(sk - skmin) + gmax; + } + + //tmp0 = 0.7*tmp0 + 0.3*gains[0]; + //tmp1 = 0.7*tmp1 + 0.3*gains[1]; + //tmp2 = 0.7*tmp2 + 0.3*gains[2]; + tmp0 = AWB_clip_value(tmp0); tmp1 = AWB_clip_value(tmp1); tmp2 = AWB_clip_value(tmp2); - tmp3 = AWB_clip_value(tmp3); - isp_params->channel_gain[0] = tmp1; - isp_params->channel_gain[1] = tmp2; - isp_params->channel_gain[2] = tmp3; + isp_params->channel_gain[0] = tmp0; + isp_params->channel_gain[1] = tmp1; + isp_params->channel_gain[2] = tmp2; } -void AWB_compute_gains(global_stats_t *gstats, isp_params_t *isp_params){ +void AWB_compute_gains_static(global_stats_t *gstats, isp_params_t *isp_params){ // Adjust AWB - float tmp0=1.35; + float tmp0=1.4; float tmp1=1.0; - float tmp2=1.35; + float tmp2=1.4; isp_params->channel_gain[0] = tmp0; isp_params->channel_gain[1] = tmp1; isp_params->channel_gain[2] = tmp2; } + +void AWB_compute_gains_white_patch(global_stats_t *gstats, isp_params_t *isp_params){ + float Rmax = (*gstats)[0].max; // RED + float Gmax = (*gstats)[1].max; // GREEN + float Bmax = (*gstats)[2].max; // BLUE + + float alfa = Gmax/Rmax; + const float beta = 0.9; + float gamma = Gmax/Bmax; + + alfa = AWB_clip_value(alfa); + gamma = AWB_clip_value(gamma); + + isp_params->channel_gain[0] = alfa; + isp_params->channel_gain[1] = beta; + isp_params->channel_gain[2] = gamma; +} + +void AWB_compute_gains_gray_world(global_stats_t *gstats, isp_params_t *isp_params){ + printf("AWB --->"); + float Ravg = (*gstats)[0].mean; // RED + float Gavg = (*gstats)[1].mean; // GREEN + float Bavg = (*gstats)[2].mean; // BLUE + + float alfa = Gavg/Ravg; + const float beta = 0.9; + float gamma = Gavg/Bavg; + + alfa = AWB_clip_value(alfa); + gamma = AWB_clip_value(gamma); + + isp_params->channel_gain[0] = alfa; + isp_params->channel_gain[1] = beta; + isp_params->channel_gain[2] = gamma; +} + + void AWB_print_gains(isp_params_t *isp_params){ printf("awb:%f,%f,%f\n", isp_params->channel_gain[0], @@ -165,12 +224,8 @@ void AWB_print_gains(isp_params_t *isp_params){ // ---------------------------------- GAMMA ------------------------------ -/** -* Apply 1.8 gamma to each pixel in an image using stride 1 -* it take 255 bytes of memory, 0MUL, 0DIV, 2MEM ACCESS -* @param buffsize - Size of the buffer to operate on. -* @param img - * Pointer to the image to operate on. Modified -*/ + +/* const uint8_t gamma_1p8_s1[255] = { 0,12,17,22,25,29,32,35,37,40,42,44,47,49,51,53,55,57,58,60,62,64,65,67,69, 70,72,73,75,76,78,79,80,82,83,85,86,87,89,90,91,92,94,95,96,97,98,100,101, @@ -185,6 +240,38 @@ const uint8_t gamma_1p8_s1[255] = { 223,224,225,225,226,226,227,228,228,229,230,230,231,231,232,233,233,234,234, 235,236,236,237,237,238,238,239,240,240,241,241,242,243,243,244,244,245,245, 246,247,247,248,248,249,249,250,251,251,252,252,253,253,254,255}; +*/ + +const uint8_t gamma_1p8_s1[255] = { +0,12,17,22,25,29,32,35,37,40,42,44,47,49,51,53,55,57,58,60,62,64,65,67,69, +70,72,73,75,76,78,79,80,82,83,85,86,87,89,90,91,92,94,95,96,97,98,100,101, +102,103,104,105,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121, +122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,139, +140,141,142,143,144,145,146,146,147,148,149,150,151,152,152,153,154,155,156, +157,157,158,159,160,161,161,162,163,164,165,165,166,167,168,169,169,170,171, +172,172,173,174,175,175,176,177,178,178,179,180,181,181,182,183,183,184,185, +186,186,187,188,188,189,190,191,191,192,193,193,194,195,195,196,197,198,198, +199,200,200,201,202,202,203,204,204,205,206,206,207,208,208,209,209,210,211, +211,212,213,213,214,215,215,216,217,217,218,218,219,220,220,221,222,222,223, +223,224,225,225,226,226,227,228,228,229,230,230,231,231,232,233,233,234,234, +235,236,236,237,237,238,238,239,240,240,241,241,242,243,243,244,244,245,245, +246,247,247,247,246,245,244,243,242,241,240,239,238,237,236,235}; + +const uint8_t gamma_1p8_s1_green[255] = { +0,12,17,22,25,29,32,35,37,40,42,44,47,49,51,53,55,57,58,60,62,64,65,67,69, +70,72,73,75,76,78,79,80,82,83,85,86,87,89,90,91,92,94,95,96,97,98,100,101, +102,103,104,105,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121, +122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,139, +140,141,142,143,144,145,146,146,147,148,149,150,151,152,152,153,154,155,156, +157,157,158,159,160,161,161,162,163,164,165,165,166,167,168,169,169,170,171, +172,172,173,174,175,175,176,177,178,178,179,180,181,181,182,183,183,184,185, +186,186,187,188,188,189,190,191,191,192,193,193,194,195,195,196,197,198,198, +199,200,200,201,202,202,203,204,204,205,206,206,207,208,208,209,209,210,211, +211,212,213,213,214,215,215,216,217,217,218,218,219,220,220,221,222,222,223, +223,224,225,225,226,226,227,228,228,229,230,230,231,231,232,233,233,234,234, +235,236,236,237,237,238,238,239,240,240,241,241,242,243,243,244,244,245,245, +246,247,247,247,246,245,244,243,242,241,240,239,238,237,236,235}; + void isp_gamma_stride1(const uint32_t buffsize, uint8_t *img){ // gamma naming: 1p8_s1 = gamma 1.8 , with a stride of 1 @@ -194,6 +281,29 @@ void isp_gamma_stride1(const uint32_t buffsize, uint8_t *img){ } } +void isp_gamma_1p8( + uint8_t * img_in, + const size_t height, + const size_t width, + const size_t channels + ) +{ + for(size_t k = 0; k < height; k++){ + for(size_t j = 0; j < width; j++){ + for(size_t c = 0; c < channels; c++){ + size_t index_in = c * (height * width) + k * width + j; + uint8_t value = img_in[index_in]; + if (c==1){ + img_in[index_in] = gamma_1p8_s1_green[value]; + } + else{ + img_in[index_in] = gamma_1p8_s1[value]; + } + + } + } + } +} // -------------------------- ROTATE/RESIZE ------------------------------------- #define img(row, col, WIDTH) img[(WIDTH) * (row) + (col)] diff --git a/camera/src/statistics.c b/camera/src/statistics.c index cfe6461f..4bfd0617 100644 --- a/camera/src/statistics.c +++ b/camera/src/statistics.c @@ -1,4 +1,4 @@ - +#include #include #include "xccompat.h" @@ -63,29 +63,33 @@ void compute_simple_stats(channel_stats_t *stats) // Calculate the histogram uint8_t temp_min = 0; uint8_t temp_max = 0; + float temp_mean = 0; for(int k = 0; k < HISTOGRAM_BIN_COUNT; k++){ - unsigned bin = stats->histogram.bins[k]; + uint32_t bin_count = stats->histogram.bins[k]; // mean - stats->mean += bin * k; + temp_mean += bin_count * k; // max and min - if (bin != 0){ + if (bin_count != 0){ temp_max = k; if (temp_min == 0){ temp_min = k; } } - //stats->max = (stats->max >= bin)? stats->max : bin; - //stats->min = (stats->min <= bin)? stats->min : bin; } - stats->max = temp_max; - stats->min = temp_min; // biased downwards due to truncation - stats->max <<= APP_HISTOGRAM_QUANTIZATION_BITS; - stats->min <<= APP_HISTOGRAM_QUANTIZATION_BITS; - stats->mean *= (1<max = (temp_max << APP_HISTOGRAM_QUANTIZATION_BITS); + stats->min = (temp_min << APP_HISTOGRAM_QUANTIZATION_BITS); + stats->mean = (temp_mean) *(1 << APP_HISTOGRAM_QUANTIZATION_BITS) * histogram_norm_factor; } +void print_simple_stats(channel_stats_t *stats){ + printf("Max: %d\n", stats->max); + printf("Min: %d\n", stats->min); + printf("Mean: %f\n", stats->mean); + printf("Skewness: %f\n", stats->skewness); + printf("Percentile: %d\n", stats->percentile); +} void find_percentile(channel_stats_t *stats, const float fraction) { @@ -136,14 +140,24 @@ void statistics_thread( compute_skewness(&global_stats[channel]); compute_simple_stats(&global_stats[channel]); find_percentile(&global_stats[channel], APP_WB_PERCENTILE); + // print_simple_stats(&global_stats[channel]); } // Adjust AE - AE_control_exposure(&global_stats, sc_if); + uint8_t ae_done = AE_control_exposure(&global_stats, sc_if); // Adjust AWB - AWB_compute_gains(&global_stats, &isp_params); + AWB_compute_gains_static(&global_stats, &isp_params); + if (ae_done == 1){ + //AWB_compute_gains_white_patch(&global_stats, &isp_params); + //AWB_compute_gains_gray_world(&global_stats, &isp_params); + //AWB_compute_gains_percentile(&global_stats, &isp_params); + //AWB_compute_gains_static(&global_stats, &isp_params); + } + // Apply gamma curve + //TODO + // Print ISP info AWB_print_gains(&isp_params); AE_print_skewness(&global_stats); diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index 80d1591e..8f570295 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -5,7 +5,9 @@ #include "io_utils.h" #include "app.h" +//#include "isp.h" +#define APPLY_GAMMA 0 void user_app() { @@ -36,6 +38,15 @@ void user_app() memcpy(temp_buffer, image_buffer, APP_IMAGE_CHANNEL_COUNT * APP_IMAGE_HEIGHT_PIXELS * APP_IMAGE_WIDTH_PIXELS * sizeof(uint8_t)); uint8_t * io_buff = (uint8_t *) &image_buffer[0][0][0]; // io_buff this will have [APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS][APP_IMAGE_CHANNEL_COUNT] dimentions + + // apply gamma correction + #if APPLY_GAMMA + isp_gamma_1p8((uint8_t *) &temp_buffer[0][0][0], + APP_IMAGE_HEIGHT_PIXELS, + APP_IMAGE_WIDTH_PIXELS, + APP_IMAGE_CHANNEL_COUNT); + #endif + swap_dimensions((uint8_t *) &temp_buffer[0][0][0], io_buff, APP_IMAGE_HEIGHT_PIXELS, APP_IMAGE_WIDTH_PIXELS, diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index 85669742..c344dc50 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -32,7 +32,7 @@ void user_app_raw(){ sizeof(image_buffer)); // Save the image to a file - write_image_file("capture.bin", (uint8_t * ) &image_buffer[0][0], + write_image_file("capture.raw", (uint8_t * ) &image_buffer[0][0], MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES, 1); printf("Image saved. Exiting.\n"); diff --git a/python/FIR_pipeline.py b/python/FIR_pipeline.py index 01f83534..c72684d6 100644 --- a/python/FIR_pipeline.py +++ b/python/FIR_pipeline.py @@ -56,6 +56,7 @@ def intercalate_zeros(filter): def convolve_h(j:int, i:int, img, filter:list): sump = 0 + KH = (len(filter) -1) //2 for u in range(-KH, KH + 1, 1): if(i+u >= img.shape[1]): u = 0 @@ -67,6 +68,7 @@ def convolve_h(j:int, i:int, img, filter:list): def convolve_v(j:int, i:int, img, filter:list): sump = 0 + KV = (len(filter) -1) //2 for u in range(-KV, KV + 1, 1): if(j+u >= img.shape[0]): u = 0 @@ -99,22 +101,10 @@ def create_filter(): return h_filter, v_filter, KV, KH -if __name__ == '__main__': - - ENABLE_IMSHOW = True - - # get test_image - img, (height, width) = get_real_image() - img = img.reshape(height, width) - - imgh = np.zeros((height//2, width//4, 3)) - imgv = np.zeros((height//4, width//4, 3)) - - # create the filters - h_filter, v_filter, KV, KH = create_filter() - print(h_filter, v_filter) - +def horizontal_filer(img, h_filter, height, width): + KH = (len(h_filter) -1) //2 # horizontal filtering + imgh = np.zeros((height//2, width//4, 3)) for j in range(height): for i in range(0, width, STEP): pos = i//STEP @@ -127,14 +117,13 @@ def create_filter(): imgh[j//2, pos, BLUE] = convolve_h(j, i+1, img, h_filter) imgh = np.clip(imgh, 0, 255).astype(np.uint8) + return imgh + - # vertical filter +def vertical_filer(imgh,height, width, v_filter, red, green, blue): +# vertical filter new_h, new_w, ch = imgh.shape - - # white balancing - imgh = gray_world(imgh) - red, green, blue = imgh[:,:,RED], imgh[:,:,GREEN], imgh[:,:,BLUE] - + imgv = np.zeros((height//4, width//4, 3)) for i in range(new_w): for j in range(0, new_h , STEP_V): red_p = convolve_v(j, i, red, v_filter) @@ -146,16 +135,80 @@ def create_filter(): imgv = np.clip(imgv, 0, 255).astype(np.uint8) - img = imgv + return imgv + + +def FIR_pipeline(img, height, width, ENABLE_IMSHOW=None): + # create the filters + h_filter, v_filter, KV, KH = create_filter() + + # horizontal filter + imgh = horizontal_filer(img, h_filter, height, width) + + # white balancing + imgh = gray_world(imgh) + red, green, blue = imgh[:,:,RED], imgh[:,:,GREEN], imgh[:,:,BLUE] + + # vertical filtering + img = vertical_filer(imgh,height, width, v_filter, red, green, blue) + + ########### post processing ############## + # black level substraction + img = normalize(img, 15, 254, np.uint8) + + # gamma + img = img ** (1.0 / 1.8) + + # sharpen (optional) + #kernel_sharpen = kernel_sharpen/np.sum(kernel_sharpen) + #img = cv2.filter2D(src=img, ddepth=-1, kernel=kernel_sharpen_5) + + # Color correction (optional) + #img = new_color_correction(img) + + # clip the image + img = np.clip(255*img, 0, 255).astype(np.uint8) + ########################################## + + if ENABLE_IMSHOW: + plt.imshow(img) + plt.show() + # save image + name = f"out.png" + #print(name) + Image.fromarray(img).save(name) # option 1 pillow + + return img + + +if __name__ == '__main__': + + ENABLE_IMSHOW = True + + # get test_image + img, (height, width) = get_real_image() + img = img.reshape(height, width) + + # create the filters + h_filter, v_filter, KV, KH = create_filter() + print(h_filter, v_filter) + # horizontal filter + imgh = horizontal_filer(img, h_filter, height, width) + + # white balancing + imgh = gray_world(imgh) + red, green, blue = imgh[:,:,RED], imgh[:,:,GREEN], imgh[:,:,BLUE] + + # vertical filtering + img = vertical_filer(imgh,height, width, v_filter, red, green, blue) - # ########### post processing ############## # black level substraction img = normalize(img, 15, 254, np.uint8) # gamma - img = img ** (1.0 / 2) + img = img ** (1.0 / 1.8) # sharpen (optional) kernel_sharpen = kernel_sharpen/np.sum(kernel_sharpen) @@ -190,4 +243,3 @@ def create_filter(): img_psnr = peak_signal_noise_ratio(ref_image, img) print(img_psnr) - diff --git a/python/compare_images.py b/python/compare_images.py new file mode 100644 index 00000000..8701a2bb --- /dev/null +++ b/python/compare_images.py @@ -0,0 +1,69 @@ +import time +import os + +import cv2 +from matplotlib import pyplot as plt +import numpy as np + +from utils import peak_signal_noise_ratio +from PIL import Image +from run_xscope_bin import * +from pathlib import Path + +from FIR_pipeline import FIR_pipeline +from utils import pipeline + + +# path definitions +cwd = str(Path(__file__).parent.resolve()) +top_level = str(Path(__file__).parent.parent.resolve()) +examples = top_level + "/build/examples" + + +def load_image(input_name, height, width, ch=0): + with open(input_name, "rb") as f: + data = f.read() + buffer = np.frombuffer(data, dtype=np.uint8) + if ch == 0: + buffer = buffer.reshape(height, width) + else: + buffer = buffer.reshape(height, width, ch) + return buffer + +if __name__ == '__main__': + ENABLE_IMSHOW = 1 + kfactor = 4 + + run(examples + '/take_picture_raw/example_take_picture_raw.xe') + time.sleep(1) + run(examples + '/take_picture_downsample/example_take_picture_downsample.xe') + time.sleep(1) + + # load + width, height, input_name = 640, 480, 'capture.raw' + img_raw = load_image(input_name, height, width) + + # highest quality + ref_image = pipeline(img_raw, False) + ref_image = cv2.resize(ref_image, (width // kfactor, height // kfactor), interpolation=cv2.INTER_AREA) + + # raw with python pipeline + img_raw_FIR = FIR_pipeline(img_raw, height, width, ENABLE_IMSHOW) + + # downsampled + input_name_dwn = 'capture.bin' + width, height = 160, 120 + img_dwn = load_image(input_name_dwn, height, width, 3) + + # show the 3 images + plt.subplot(1, 3, 1) + plt.imshow(ref_image) + plt.subplot(1, 3, 2) + plt.imshow(img_raw_FIR) + plt.subplot(1, 3, 3) + plt.imshow(img_dwn) + + # PSNR + img_psnr = peak_signal_noise_ratio(ref_image, img_raw_FIR) + print(img_psnr) + diff --git a/python/decode_raw8.py b/python/decode_raw8.py index db114f52..e8b531b2 100644 --- a/python/decode_raw8.py +++ b/python/decode_raw8.py @@ -27,8 +27,7 @@ plot_imgs ) -input_name = os.getenv('BINARY_IMG_PATH') or "capture.bin" -#input_name = Path(__file__).parent / "capture.bin" +input_name = os.getenv('BINARY_IMG_PATH') or "capture.raw" width, height = 640, 480 diff --git a/utils/xscope_fileio b/utils/xscope_fileio index cca94745..64837d55 160000 --- a/utils/xscope_fileio +++ b/utils/xscope_fileio @@ -1 +1 @@ -Subproject commit cca94745b63b3f7bbc93fc34f2111a196a8937df +Subproject commit 64837d5530d55ac928a9dae7e38d4bb4edc38361 From f2a07bc97d97280a286b1cd8606a3130ff1351c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Thu, 15 Jun 2023 18:01:27 +0100 Subject: [PATCH 098/306] passing to the camera api --- .gitignore | 3 ++- camera/api/camera_api.h | 7 ++++++ camera/src/camera_api.c | 24 +++++++++++++++++--- camera/src/camera_main.xc | 2 +- camera/src/packet_handler.c | 26 +++++++++++++++++----- camera/src/statistics.c | 5 +++++ examples/take_picture_downsample/src/app.c | 2 ++ utils/xscope_fileio | 2 +- 8 files changed, 60 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 1680b94a..66040fff 100644 --- a/.gitignore +++ b/.gitignore @@ -83,4 +83,5 @@ temp # dotenv .env -**/*.png \ No newline at end of file +**/*.png +*.bmp diff --git a/camera/api/camera_api.h b/camera/api/camera_api.h index 8ad4bf8b..6239d680 100644 --- a/camera/api/camera_api.h +++ b/camera/api/camera_api.h @@ -1,7 +1,12 @@ #pragma once + +// xcore +// #include #include "xccompat.h" +// user #include "sensor.h" + #if defined(__XC__) || defined(__cplusplus) extern "C" { #endif @@ -12,6 +17,8 @@ extern "C" { * Initialize the camera API. Must be called before any other API functions. */ void camera_api_init(); +void camera_stop(); +unsigned camera_api_check_stop(); /** * SERVER SIDE diff --git a/camera/src/camera_api.c b/camera/src/camera_api.c index 8b9c5b77..cfa5a51c 100644 --- a/camera/src/camera_api.c +++ b/camera/src/camera_api.c @@ -12,15 +12,33 @@ #define CHAN_RAW 0 #define CHAN_DEC 1 +#define CHAN_STOP 2 // In order to interface the handler and api -streaming_channel_t c_user_api[2]; +streaming_channel_t c_user_api[3]; void camera_api_init() { - c_user_api[CHAN_RAW] = s_chan_alloc(); - c_user_api[CHAN_DEC] = s_chan_alloc(); + c_user_api[CHAN_RAW] = s_chan_alloc(); + c_user_api[CHAN_DEC] = s_chan_alloc(); + c_user_api[CHAN_STOP] = s_chan_alloc(); +} + +unsigned camera_api_check_stop(){ + SELECT_RES( + CASE_THEN(c_user_api[CHAN_STOP].end_b, user_handler), + DEFAULT_THEN(default_handler)) + { + user_handler: + return 1; + default_handler: + return 0; + } +} + +void camera_stop(){ + s_chan_out_word(c_user_api[CHAN_STOP].end_a, (unsigned) 1); } void camera_api_new_row_raw( diff --git a/camera/src/camera_main.xc b/camera/src/camera_main.xc index 72da444f..03499040 100644 --- a/camera/src/camera_main.xc +++ b/camera/src/camera_main.xc @@ -32,7 +32,7 @@ void camera_main( streaming chan c_stat_thread; sensor_control_if sc_if; - // set the channels for the camera api + // set the channel for the camera api camera_api_init(); // Assign lanes and polarities diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index 9f8ba577..ec414eb8 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -4,6 +4,7 @@ #include #include +#include #include "packet_handler.h" #include "image_vfilter.h" @@ -264,7 +265,6 @@ void handle_packet( } } - /** * Top level of the packet handling thread. Receives MIPI packets from the * packet receiver and passes them to `handle_packet()` for parsing and @@ -283,7 +283,8 @@ void mipi_packet_handler( mipi_packet_t packet_buffer[MIPI_PKT_BUFFER_COUNT]; unsigned pkt_idx = 0; - camera_api_init(); + // allocate camera api channends + // camera_api_init(); // Give the MIPI packet receiver a first buffer s_chan_out_word(c_pkt, (unsigned) &packet_buffer[pkt_idx] ); @@ -291,11 +292,26 @@ void mipi_packet_handler( while(1) { pkt_idx = (pkt_idx + 1) & (MIPI_PKT_BUFFER_COUNT-1); - mipi_packet_t * pkt = (mipi_packet_t*) s_chan_in_word(c_pkt); // Swap buffers with the receiver thread. Give it the next buffer // to fill and take the last filled buffer from it. - s_chan_out_word(c_pkt, (unsigned) &packet_buffer[pkt_idx] ); - + mipi_packet_t * pkt = (mipi_packet_t*) s_chan_in_word(c_pkt); + + // Check is we are supose to stop or continue + unsigned stop = camera_api_check_stop(); + + if (stop == 1){ + // send stop to MipiReciever + s_chan_out_word(c_pkt, (unsigned) NULL); + // send stop to statistics + s_chan_out_word(c_out_row, (unsigned) 1); + // end thread + printf("\n\nMipiPacketHandler: stop\n\n"); + return; + } + else{ + // send info to MipiReciever + s_chan_out_word(c_pkt, (unsigned) &packet_buffer[pkt_idx]); + } // Process the packet //const mipi_header_t header = pkt->header; //const mipi_data_type_t data_type = MIPI_GET_DATA_TYPE(header); diff --git a/camera/src/statistics.c b/camera/src/statistics.c index cfe6461f..eba5f253 100644 --- a/camera/src/statistics.c +++ b/camera/src/statistics.c @@ -125,6 +125,11 @@ void statistics_thread( if(row == NULL) // Signal end of frame [1] break; + if(row == (low_res_image_row_t *) 1) + { + return; + } + // Update histogram for(uint8_t channel = 0; channel < APP_IMAGE_CHANNEL_COUNT; channel++){ update_histogram(&global_stats[channel].histogram, &row->pixels[channel][0]); diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index 80d1591e..dae2292b 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -24,6 +24,8 @@ void user_app() } printf("Image captured...\n"); + camera_stop(); + delay_milliseconds(100); // Rotate 180 degrees // rotate_image(image_buffer); diff --git a/utils/xscope_fileio b/utils/xscope_fileio index cca94745..99e47d6f 160000 --- a/utils/xscope_fileio +++ b/utils/xscope_fileio @@ -1 +1 @@ -Subproject commit cca94745b63b3f7bbc93fc34f2111a196a8937df +Subproject commit 99e47d6f419e9b9d73de889d03290218539764e8 From 280549d763716b799681a8dc0e95dcb234f6a95f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Fri, 16 Jun 2023 10:46:50 +0100 Subject: [PATCH 099/306] adding stop to camera --- camera/api/camera_api.h | 16 ++++++++++++- camera/src/camera_api.c | 2 +- camera/src/camera_main.xc | 12 +++------- camera/src/statistics.c | 6 ++++- examples/take_picture_downsample/src/app.c | 2 +- examples/take_picture_downsample/src/main.xc | 1 - examples/take_picture_raw/src/app_raw.c | 2 +- sensors/_sony_imx219/imx219.h | 12 ++++------ sensors/api/sensor_control.h | 8 ++++++- sensors/src/sensor_control.xc | 24 ++++++++++++++++++++ 10 files changed, 62 insertions(+), 23 deletions(-) diff --git a/camera/api/camera_api.h b/camera/api/camera_api.h index 6239d680..d6c0d398 100644 --- a/camera/api/camera_api.h +++ b/camera/api/camera_api.h @@ -17,7 +17,21 @@ extern "C" { * Initialize the camera API. Must be called before any other API functions. */ void camera_api_init(); -void camera_stop(); + +/** + * SERVER SIDE + * + * Stop the camera API. Must be called before exiting the program. + */ +void camera_api_stop(); + +/** + * SERVER SIDE + * + * Check if the client has requested to stop the camera. + * + * @return 1 if the client has requested to stop the camera, 0 otherwise + */ unsigned camera_api_check_stop(); /** diff --git a/camera/src/camera_api.c b/camera/src/camera_api.c index cfa5a51c..9e2c7d58 100644 --- a/camera/src/camera_api.c +++ b/camera/src/camera_api.c @@ -37,7 +37,7 @@ unsigned camera_api_check_stop(){ } } -void camera_stop(){ +void camera_api_stop(){ s_chan_out_word(c_user_api[CHAN_STOP].end_a, (unsigned) 1); } diff --git a/camera/src/camera_main.xc b/camera/src/camera_main.xc index 03499040..2d134ae4 100644 --- a/camera/src/camera_main.xc +++ b/camera/src/camera_main.xc @@ -57,16 +57,10 @@ void camera_main( MIPI_CLK_DIV, MIPI_CFG_CLK_DIV); - // Start camera and its configurations - int r = 0; - r |= camera_init(i2c); - delay_milliseconds(100); - r |= camera_configure(i2c); - delay_milliseconds(600); - r |= camera_start(i2c); - assert(r == 0); // assert that camera is started and configured + // Initialize camera and its configurations + sensor_initialize(i2c); printf("\nCamera_started and configured...\n"); - delay_milliseconds(2000); + delay_milliseconds(1000); // start the different jobs (packet controller, handler, and post_process) par diff --git a/camera/src/statistics.c b/camera/src/statistics.c index eba5f253..737d90ef 100644 --- a/camera/src/statistics.c +++ b/camera/src/statistics.c @@ -122,11 +122,15 @@ void statistics_thread( low_res_image_row_t* row = (low_res_image_row_t*) s_chan_in_word(c_img_in); - if(row == NULL) // Signal end of frame [1] + if(row == NULL){ // Signal end of frame [1] break; + } if(row == (low_res_image_row_t *) 1) { + // stop the camera sensor + sensor_control_stop(sc_if); + // exit return; } diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index dae2292b..9ff76f61 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -24,7 +24,7 @@ void user_app() } printf("Image captured...\n"); - camera_stop(); + camera_api_stop(); delay_milliseconds(100); // Rotate 180 degrees // rotate_image(image_buffer); diff --git a/examples/take_picture_downsample/src/main.xc b/examples/take_picture_downsample/src/main.xc index 5099168d..50e5caf7 100644 --- a/examples/take_picture_downsample/src/main.xc +++ b/examples/take_picture_downsample/src/main.xc @@ -1,4 +1,3 @@ -// Copyright (c) 2020, XMOS Ltd, All rights reserved #include #include #include diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index 85669742..fa562180 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -25,7 +25,7 @@ void user_app_raw(){ exit(1); } printf("Image captured...\n"); - + camera_api_stop(); // Convert image from int8 to uint8 in-place vect_int8_to_uint8((uint8_t*) image_buffer, (int8_t*) image_buffer, diff --git a/sensors/_sony_imx219/imx219.h b/sensors/_sony_imx219/imx219.h index 54d1e07f..d3c27394 100644 --- a/sensors/_sony_imx219/imx219.h +++ b/sensors/_sony_imx219/imx219.h @@ -64,10 +64,8 @@ int imx219_read(CLIENT_INTERFACE(i2c_master_if, i2c), uint16_t addr); void imx219_read_gains(CLIENT_INTERFACE(i2c_master_if, i2c), uint16_t values[5]); -#define camera_init(X) imx219_init(X) -#define camera_start(X) imx219_stream_start(X) -#define camera_configure(X) imx219_configure_mode(X) -#define camera_set_exposure(iic,ex) imx219_set_gain_dB(iic,ex) - - - +#define camera_init(iic) imx219_init(iic) +#define camera_start(iic) imx219_stream_start(iic) +#define camera_stop(iic) imx219_stream_stop(iic) +#define camera_configure(iic) imx219_configure_mode(iic) +#define camera_set_exposure(iic,ex) imx219_set_gain_dB(iic,ex) diff --git a/sensors/api/sensor_control.h b/sensors/api/sensor_control.h index cbe44ac4..0f1e017d 100644 --- a/sensors/api/sensor_control.h +++ b/sensors/api/sensor_control.h @@ -8,16 +8,22 @@ typedef interface sensor_control_if { void set_exposure(unsigned exposure); + void stop(); } sensor_control_if; void sensor_control( server interface sensor_control_if sc, client interface i2c_master_if i2c); +void sensor_initialize(client interface i2c_master_if i2c); + #endif /* These wrappers are for calling client interface functions from C */ void sensor_control_set_exposure( CLIENT_INTERFACE(sensor_control_if, sc), const unsigned exposure - ); \ No newline at end of file + ); + +void sensor_control_stop( + CLIENT_INTERFACE(sensor_control_if, sc)); diff --git a/sensors/src/sensor_control.xc b/sensors/src/sensor_control.xc index f82cc184..d73d17f1 100644 --- a/sensors/src/sensor_control.xc +++ b/sensors/src/sensor_control.xc @@ -1,20 +1,38 @@ #include #include #include +#include #include "i2c.h" #include "sensor_control.h" #include "sensor.h" + +void sensor_initialize(client interface i2c_master_if i2c){ + int r = 0; + r |= camera_init(i2c); + delay_milliseconds(100); + r |= camera_configure(i2c); + delay_milliseconds(600); + r |= camera_start(i2c); + assert((r == 0)); // assert that camera is started and configured +} + + void sensor_control( server interface sensor_control_if sc, client interface i2c_master_if i2c) { while(1){ select { + case sc.set_exposure(unsigned exposure): camera_set_exposure(i2c, exposure); break; + + case sc.stop(): + camera_stop(i2c); + break; } } } @@ -26,3 +44,9 @@ void sensor_control_set_exposure( { sc.set_exposure(exposure); } + +void sensor_control_stop( + client interface sensor_control_if sc) +{ + sc.stop(); +} From cd8d155a708e19f87da87aab527f8b42d5b1014f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Fri, 16 Jun 2023 11:29:14 +0100 Subject: [PATCH 100/306] api init in camera api --- camera/api/camera_api.h | 5 ++--- camera/src/camera_main.xc | 3 --- camera/src/packet_handler.c | 3 --- examples/take_picture_downsample/src/app.c | 8 ++++++-- examples/take_picture_raw/src/app_raw.c | 10 ++++++++-- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/camera/api/camera_api.h b/camera/api/camera_api.h index d6c0d398..1078621e 100644 --- a/camera/api/camera_api.h +++ b/camera/api/camera_api.h @@ -1,7 +1,6 @@ #pragma once // xcore -// #include #include "xccompat.h" // user #include "sensor.h" @@ -12,14 +11,14 @@ extern "C" { #endif /** - * SERVER SIDE + * CLIENT SIDE * * Initialize the camera API. Must be called before any other API functions. */ void camera_api_init(); /** - * SERVER SIDE + * CLIENT SIDE * * Stop the camera API. Must be called before exiting the program. */ diff --git a/camera/src/camera_main.xc b/camera/src/camera_main.xc index 2d134ae4..0d45b9ea 100644 --- a/camera/src/camera_main.xc +++ b/camera/src/camera_main.xc @@ -31,9 +31,6 @@ void camera_main( streaming chan c_ctrl; streaming chan c_stat_thread; sensor_control_if sc_if; - - // set the channel for the camera api - camera_api_init(); // Assign lanes and polarities write_node_config_reg(mipi_tile, diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index ec414eb8..9d406bd0 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -283,9 +283,6 @@ void mipi_packet_handler( mipi_packet_t packet_buffer[MIPI_PKT_BUFFER_COUNT]; unsigned pkt_idx = 0; - // allocate camera api channends - // camera_api_init(); - // Give the MIPI packet receiver a first buffer s_chan_out_word(c_pkt, (unsigned) &packet_buffer[pkt_idx] ); diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index 9ff76f61..718d8f8f 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -9,6 +9,10 @@ void user_app() { + + // Initialize camera api + camera_api_init(); + int8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; uint8_t temp_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; @@ -18,16 +22,16 @@ void user_app() // Wait for the image to set exposure delay_milliseconds(5000); printf("Requesting image...\n"); + // grab a frame if(camera_capture_image(image_buffer)){ printf("Error capturing image\n"); exit(1); } printf("Image captured...\n"); + // stop the threads and camera stream camera_api_stop(); delay_milliseconds(100); - // Rotate 180 degrees - // rotate_image(image_buffer); // convert to uint8 with right dimentions // convert to uint8 diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index fa562180..b539deb8 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -9,8 +9,11 @@ #include "app_raw.h" #include "io_utils.h" -void user_app_raw(){ - +void user_app_raw() +{ + // Initialize camera api + camera_api_init(); + // set the input image to 0 int8_t image_buffer[H_RAW][W_RAW]; memset(image_buffer, -128, H_RAW * W_RAW); @@ -25,7 +28,10 @@ void user_app_raw(){ exit(1); } printf("Image captured...\n"); + + // stop the threads and camera stream camera_api_stop(); + // Convert image from int8 to uint8 in-place vect_int8_to_uint8((uint8_t*) image_buffer, (int8_t*) image_buffer, From cf7d5da7551bac4389d9c99cbc2b46f046287f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Fri, 16 Jun 2023 11:42:42 +0100 Subject: [PATCH 101/306] adding test file --- python/test_downs_vs_raw.py | 69 +++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 python/test_downs_vs_raw.py diff --git a/python/test_downs_vs_raw.py b/python/test_downs_vs_raw.py new file mode 100644 index 00000000..8701a2bb --- /dev/null +++ b/python/test_downs_vs_raw.py @@ -0,0 +1,69 @@ +import time +import os + +import cv2 +from matplotlib import pyplot as plt +import numpy as np + +from utils import peak_signal_noise_ratio +from PIL import Image +from run_xscope_bin import * +from pathlib import Path + +from FIR_pipeline import FIR_pipeline +from utils import pipeline + + +# path definitions +cwd = str(Path(__file__).parent.resolve()) +top_level = str(Path(__file__).parent.parent.resolve()) +examples = top_level + "/build/examples" + + +def load_image(input_name, height, width, ch=0): + with open(input_name, "rb") as f: + data = f.read() + buffer = np.frombuffer(data, dtype=np.uint8) + if ch == 0: + buffer = buffer.reshape(height, width) + else: + buffer = buffer.reshape(height, width, ch) + return buffer + +if __name__ == '__main__': + ENABLE_IMSHOW = 1 + kfactor = 4 + + run(examples + '/take_picture_raw/example_take_picture_raw.xe') + time.sleep(1) + run(examples + '/take_picture_downsample/example_take_picture_downsample.xe') + time.sleep(1) + + # load + width, height, input_name = 640, 480, 'capture.raw' + img_raw = load_image(input_name, height, width) + + # highest quality + ref_image = pipeline(img_raw, False) + ref_image = cv2.resize(ref_image, (width // kfactor, height // kfactor), interpolation=cv2.INTER_AREA) + + # raw with python pipeline + img_raw_FIR = FIR_pipeline(img_raw, height, width, ENABLE_IMSHOW) + + # downsampled + input_name_dwn = 'capture.bin' + width, height = 160, 120 + img_dwn = load_image(input_name_dwn, height, width, 3) + + # show the 3 images + plt.subplot(1, 3, 1) + plt.imshow(ref_image) + plt.subplot(1, 3, 2) + plt.imshow(img_raw_FIR) + plt.subplot(1, 3, 3) + plt.imshow(img_dwn) + + # PSNR + img_psnr = peak_signal_noise_ratio(ref_image, img_raw_FIR) + print(img_psnr) + From 4b1f5c9750f1abdcdb4475929a253db2fbfb3e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Fri, 16 Jun 2023 13:49:28 +0100 Subject: [PATCH 102/306] small corrections imx and test raw,vs downsampled --- .gitignore | 3 + examples/take_picture_raw/src/app_raw.c | 3 +- python/FIR_pipeline.py | 111 ++++++++++++++++++------ python/run_xscope_bin.py | 27 +++++- python/test_downs_vs_raw.py | 48 ++++++---- sensors/_sony_imx219/imx219.h | 7 -- sensors/_sony_imx219/imx219.xc | 2 +- sensors/_sony_imx219/imx219_reg.h | 11 --- sensors/api/sensor.h | 13 +++ 9 files changed, 158 insertions(+), 67 deletions(-) diff --git a/.gitignore b/.gitignore index 66040fff..3b2999f4 100644 --- a/.gitignore +++ b/.gitignore @@ -83,5 +83,8 @@ temp # dotenv .env + +# images **/*.png *.bmp +*.raw diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index b539deb8..61009d73 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -31,6 +31,7 @@ void user_app_raw() // stop the threads and camera stream camera_api_stop(); + delay_milliseconds(100); // Convert image from int8 to uint8 in-place vect_int8_to_uint8((uint8_t*) image_buffer, @@ -38,7 +39,7 @@ void user_app_raw() sizeof(image_buffer)); // Save the image to a file - write_image_file("capture.bin", (uint8_t * ) &image_buffer[0][0], + write_image_file("capture.raw", (uint8_t * ) &image_buffer[0][0], MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES, 1); printf("Image saved. Exiting.\n"); diff --git a/python/FIR_pipeline.py b/python/FIR_pipeline.py index 01f83534..938de8cb 100644 --- a/python/FIR_pipeline.py +++ b/python/FIR_pipeline.py @@ -4,7 +4,13 @@ from PIL import Image # just to avoid color BGR issues when writting from scipy import signal -from utils import * +from utils import ( + peak_signal_noise_ratio, + pipeline, + normalize, + gray_world, + get_real_image, + new_color_correction) OFF = 0 STEP = 4 @@ -56,6 +62,7 @@ def intercalate_zeros(filter): def convolve_h(j:int, i:int, img, filter:list): sump = 0 + KH = (len(filter) -1) //2 for u in range(-KH, KH + 1, 1): if(i+u >= img.shape[1]): u = 0 @@ -67,6 +74,7 @@ def convolve_h(j:int, i:int, img, filter:list): def convolve_v(j:int, i:int, img, filter:list): sump = 0 + KV = (len(filter) -1) //2 for u in range(-KV, KV + 1, 1): if(j+u >= img.shape[0]): u = 0 @@ -99,22 +107,10 @@ def create_filter(): return h_filter, v_filter, KV, KH -if __name__ == '__main__': - - ENABLE_IMSHOW = True - - # get test_image - img, (height, width) = get_real_image() - img = img.reshape(height, width) - - imgh = np.zeros((height//2, width//4, 3)) - imgv = np.zeros((height//4, width//4, 3)) - - # create the filters - h_filter, v_filter, KV, KH = create_filter() - print(h_filter, v_filter) - +def horizontal_filer(img, h_filter, height, width): + KH = (len(h_filter) -1) //2 # horizontal filtering + imgh = np.zeros((height//2, width//4, 3)) for j in range(height): for i in range(0, width, STEP): pos = i//STEP @@ -127,14 +123,13 @@ def create_filter(): imgh[j//2, pos, BLUE] = convolve_h(j, i+1, img, h_filter) imgh = np.clip(imgh, 0, 255).astype(np.uint8) + return imgh + - # vertical filter +def vertical_filer(imgh,height, width, v_filter, red, green, blue): +# vertical filter new_h, new_w, ch = imgh.shape - - # white balancing - imgh = gray_world(imgh) - red, green, blue = imgh[:,:,RED], imgh[:,:,GREEN], imgh[:,:,BLUE] - + imgv = np.zeros((height//4, width//4, 3)) for i in range(new_w): for j in range(0, new_h , STEP_V): red_p = convolve_v(j, i, red, v_filter) @@ -146,16 +141,80 @@ def create_filter(): imgv = np.clip(imgv, 0, 255).astype(np.uint8) - img = imgv + return imgv + + +def FIR_pipeline(img, height, width, ENABLE_IMSHOW=None): + # create the filters + h_filter, v_filter, KV, KH = create_filter() + + # horizontal filter + imgh = horizontal_filer(img, h_filter, height, width) + + # white balancing + imgh = gray_world(imgh) + red, green, blue = imgh[:,:,RED], imgh[:,:,GREEN], imgh[:,:,BLUE] + + # vertical filtering + img = vertical_filer(imgh,height, width, v_filter, red, green, blue) + + ########### post processing ############## + # black level substraction + img = normalize(img, 15, 254, np.uint8) + + # gamma + img = img ** (1.0 / 1.8) + + # sharpen (optional) + #kernel_sharpen = kernel_sharpen/np.sum(kernel_sharpen) + #img = cv2.filter2D(src=img, ddepth=-1, kernel=kernel_sharpen_5) + + # Color correction (optional) + img = new_color_correction(img) + + # clip the image + img = np.clip(255*img, 0, 255).astype(np.uint8) + ########################################## + + if ENABLE_IMSHOW: + plt.imshow(img) + plt.show() + # save image + name = f"out.png" + #print(name) + Image.fromarray(img).save(name) # option 1 pillow + + return img + + +if __name__ == '__main__': + + ENABLE_IMSHOW = True + + # get test_image + img, (height, width) = get_real_image() + img = img.reshape(height, width) + + # create the filters + h_filter, v_filter, KV, KH = create_filter() + print(h_filter, v_filter) + # horizontal filter + imgh = horizontal_filer(img, h_filter, height, width) + + # white balancing + imgh = gray_world(imgh) + red, green, blue = imgh[:,:,RED], imgh[:,:,GREEN], imgh[:,:,BLUE] + + # vertical filtering + img = vertical_filer(imgh,height, width, v_filter, red, green, blue) - # ########### post processing ############## # black level substraction img = normalize(img, 15, 254, np.uint8) # gamma - img = img ** (1.0 / 2) + img = img ** (1.0 / 1.8) # sharpen (optional) kernel_sharpen = kernel_sharpen/np.sum(kernel_sharpen) @@ -189,5 +248,3 @@ def create_filter(): img_psnr = peak_signal_noise_ratio(ref_image, img) print(img_psnr) - - diff --git a/python/run_xscope_bin.py b/python/run_xscope_bin.py index 0bbf6f1c..5d423f4e 100644 --- a/python/run_xscope_bin.py +++ b/python/run_xscope_bin.py @@ -4,6 +4,9 @@ import argparse import shutil import subprocess +import glob +from pathlib import Path + def get_adapter_id(): try: @@ -11,6 +14,9 @@ def get_adapter_id(): except subprocess.CalledProcessError as e: print('Error: %s' % e.output) assert False + except FileNotFoundError: + msg = ("please ensure you have xcc tools activated in your environment") + assert False, msg xrun_out = xrun_out.split('\n') # Check that the first 4 lines of xrun_out match the expected lines @@ -70,8 +76,23 @@ def run(xe, return_stdout=False): stdout = ff.readlines() return stdout +def choose_file_with_extension(folder_path, extension): + # Get a list of files with the specified extension in the folder + files = list(Path(folder_path).rglob(f"*{extension}")) + files = list(map(str, files)) # i dont like to much this map here but it works + assert len(files) > 0 , (f"No {extension} files found in the folder.") + + [print(i,file) for i,file in enumerate(files)] + + choose = input("Choose the file to run: \n") + if int(choose) in range(len(files)): + run(files[int(choose)]) + if __name__ == "__main__": args = parse_arguments() - assert args.xe is not None, "Specify vaild .xe file" - - run(args.xe) + if (args.xe is None): + build_folder = Path(__file__).parent.parent.resolve() / "build" + print(build_folder) + choose_file_with_extension(build_folder, ".xe") + else: + run(args.xe) diff --git a/python/test_downs_vs_raw.py b/python/test_downs_vs_raw.py index 8701a2bb..cc6e3599 100644 --- a/python/test_downs_vs_raw.py +++ b/python/test_downs_vs_raw.py @@ -31,38 +31,52 @@ def load_image(input_name, height, width, ch=0): return buffer if __name__ == '__main__': - ENABLE_IMSHOW = 1 - kfactor = 4 + ENABLE_IMSHOW = False + RUN_XE = True + kfactor = 4 - run(examples + '/take_picture_raw/example_take_picture_raw.xe') - time.sleep(1) - run(examples + '/take_picture_downsample/example_take_picture_downsample.xe') - time.sleep(1) + if RUN_XE: + run(examples + '/take_picture_raw/example_take_picture_raw.xe') + time.sleep(1) + run(examples + '/take_picture_downsample/example_take_picture_downsample.xe') + time.sleep(1) - # load + # base image raw 640x480 width, height, input_name = 640, 480, 'capture.raw' img_raw = load_image(input_name, height, width) - # highest quality + # 1 - raw with decode 8 pipeline ref_image = pipeline(img_raw, False) ref_image = cv2.resize(ref_image, (width // kfactor, height // kfactor), interpolation=cv2.INTER_AREA) - # raw with python pipeline + # 2 - raw with simplified FIR pipeline img_raw_FIR = FIR_pipeline(img_raw, height, width, ENABLE_IMSHOW) - # downsampled + # 3 - downsampled image in the explorer board input_name_dwn = 'capture.bin' width, height = 160, 120 img_dwn = load_image(input_name_dwn, height, width, 3) - # show the 3 images - plt.subplot(1, 3, 1) - plt.imshow(ref_image) - plt.subplot(1, 3, 2) - plt.imshow(img_raw_FIR) - plt.subplot(1, 3, 3) - plt.imshow(img_dwn) + ############################################## + # Show the 3 imageslist( + fig, axes = plt.subplots(2, 3) + titles = [ + "RAW decode8", + "RAW python FIR", + "Downsampled\nXcore pipeline" + ] + imgs = [ref_image, img_raw_FIR, img_dwn] + for i, title in enumerate(titles): + axes[0,i].set_title(title) + axes[0,i].imshow(imgs[i]) + axes[1,i].set_ylim([0, 3000]) + axes[1,i].hist(imgs[i].ravel(), bins=256, color='gray', alpha=0.5) + + + plt.subplots_adjust(wspace=0.05) + list(map(lambda axi: axi.set_axis_off(), axes.ravel()[0:3])) + plt.show() # PSNR img_psnr = peak_signal_noise_ratio(ref_image, img_raw_FIR) print(img_psnr) diff --git a/sensors/_sony_imx219/imx219.h b/sensors/_sony_imx219/imx219.h index d3c27394..317fffbb 100644 --- a/sensors/_sony_imx219/imx219.h +++ b/sensors/_sony_imx219/imx219.h @@ -62,10 +62,3 @@ int imx219_set_gain_dB(CLIENT_INTERFACE(i2c_master_if, i2c), uint32_t dBGain); int imx219_set_binning(CLIENT_INTERFACE(i2c_master_if, i2c), uint32_t H_binning, uint32_t V_binning); int imx219_read(CLIENT_INTERFACE(i2c_master_if, i2c), uint16_t addr); void imx219_read_gains(CLIENT_INTERFACE(i2c_master_if, i2c), uint16_t values[5]); - - -#define camera_init(iic) imx219_init(iic) -#define camera_start(iic) imx219_stream_start(iic) -#define camera_stop(iic) imx219_stream_stop(iic) -#define camera_configure(iic) imx219_configure_mode(iic) -#define camera_set_exposure(iic,ex) imx219_set_gain_dB(iic,ex) diff --git a/sensors/_sony_imx219/imx219.xc b/sensors/_sony_imx219/imx219.xc index 055fa876..094b4365 100644 --- a/sensors/_sony_imx219/imx219.xc +++ b/sensors/_sony_imx219/imx219.xc @@ -141,7 +141,7 @@ int imx219_stream_start(client interface i2c_master_if i2c){ } int imx219_stream_stop(client interface i2c_master_if i2c){ - return i2c_write_table(i2c, stop, sizeof(stop) / sizeof(stop[0])); + return i2c_write_table(i2c, stop_regs, sizeof(stop_regs) / sizeof(stop_regs[0])); } int imx219_set_gain_dB(client interface i2c_master_if i2c, diff --git a/sensors/_sony_imx219/imx219_reg.h b/sensors/_sony_imx219/imx219_reg.h index bbe9bb5c..c2a1bd8a 100644 --- a/sensors/_sony_imx219/imx219_reg.h +++ b/sensors/_sony_imx219/imx219_reg.h @@ -203,15 +203,6 @@ static imx219_settings_t binning_regs[] = { {BINNING_MODE_REG, BINNING_MODE} }; - -static imx219_settings_t start[] = { - {0x0100, 0x01}, /* mode select streaming on */ -}; - -static imx219_settings_t stop[] = { - {0x0100, 0x00}, /* mode select streaming off */ -}; - static imx219_settings_t start_regs[] = { {0x0100, 0x01}, /* mode select streaming on */ }; @@ -322,5 +313,3 @@ static uint16_t gain_digital_gains[DIGITAL_GAINS + 1] = { 0x0e20, 0x0fd9, }; - - diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index 62c4a724..2dd1aeed 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -28,10 +28,23 @@ // Include custom libraries #if CONFIG_IMX219_SUPPORT #include "imx219.h" + #define camera_init(iic) imx219_init(iic) + #define camera_start(iic) imx219_stream_start(iic) + #define camera_stop(iic) imx219_stream_stop(iic) + #define camera_configure(iic) imx219_configure_mode(iic) + #define camera_set_exposure(iic,ex) imx219_set_gain_dB(iic,ex) + #endif #if CONFIG_GC2145_SUPPORT #include "gc2145.h" + /* //TODO + #define camera_init(iic) gcinit(iic) + #define camera_start(iic) gcstart(iic) + #define camera_stop(iic) gcstop(iic) + #define camera_configure(iic) gcconfigure(iic) + #define camera_set_exposure(iic,ex) gcsetexp(iic,ex) + */ #endif From be6d2ea651b3765e489b73a3a516ac90c84c7365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Fri, 16 Jun 2023 15:14:36 +0100 Subject: [PATCH 103/306] adding test donwsampled vs raw --- python/FIR_pipeline.py | 48 +++++++++-- python/decode_raw8.py | 2 +- python/run_xscope_bin.py | 1 - python/test_downs_vs_raw.py | 83 ------------------ python/utils.py | 63 ++++++++++++++ .../raw_vs_downsampled/test_downs_vs_raw.py | 86 +++++++++++++++++++ 6 files changed, 190 insertions(+), 93 deletions(-) delete mode 100644 python/test_downs_vs_raw.py create mode 100644 tests/hardware_tests/raw_vs_downsampled/test_downs_vs_raw.py diff --git a/python/FIR_pipeline.py b/python/FIR_pipeline.py index 938de8cb..a310b08c 100644 --- a/python/FIR_pipeline.py +++ b/python/FIR_pipeline.py @@ -10,7 +10,9 @@ normalize, gray_world, get_real_image, - new_color_correction) + new_color_correction, + run_histogram_equalization, + iterative_wb) OFF = 0 STEP = 4 @@ -96,7 +98,7 @@ def create_filter(): v1 = [0.08626086, 0.23894391, 0.34959045, 0.23894391, 0.08626086] v2 = [0.0248892, 0.2528858, 0.44445 , 0.2528858, 0.0248892] - h_filter = h1 + h_filter = h2 v_filter = v2 @@ -144,7 +146,7 @@ def vertical_filer(imgh,height, width, v_filter, red, green, blue): return imgv -def FIR_pipeline(img, height, width, ENABLE_IMSHOW=None): +def FIR_pipeline_func(img, height, width, show=False): # create the filters h_filter, v_filter, KV, KH = create_filter() @@ -152,7 +154,10 @@ def FIR_pipeline(img, height, width, ENABLE_IMSHOW=None): imgh = horizontal_filer(img, h_filter, height, width) # white balancing - imgh = gray_world(imgh) + # imgh = gray_world(imgh) + imgh = iterative_wb(imgh) + + red, green, blue = imgh[:,:,RED], imgh[:,:,GREEN], imgh[:,:,BLUE] # vertical filtering @@ -160,23 +165,31 @@ def FIR_pipeline(img, height, width, ENABLE_IMSHOW=None): ########### post processing ############## # black level substraction - img = normalize(img, 15, 254, np.uint8) + BLACK_LEVEL = 30 + img = normalize(img, BLACK_LEVEL, 254, np.uint8) # gamma - img = img ** (1.0 / 1.8) + # img = img ** (1.0 / 1.8) # sharpen (optional) #kernel_sharpen = kernel_sharpen/np.sum(kernel_sharpen) #img = cv2.filter2D(src=img, ddepth=-1, kernel=kernel_sharpen_5) # Color correction (optional) - img = new_color_correction(img) + # img = new_color_correction(img) # clip the image img = np.clip(255*img, 0, 255).astype(np.uint8) + + # image stretch + # img = stretch_histogram(img) + + # histeq + img = run_histogram_equalization(img) + ########################################## - if ENABLE_IMSHOW: + if show: plt.imshow(img) plt.show() # save image @@ -186,6 +199,23 @@ def FIR_pipeline(img, height, width, ENABLE_IMSHOW=None): return img +def stretch_histogram(image): + stretched_image = np.zeros_like(image) + + for i in range(3): # Iterate over color channels (R, G, B) + channel = image[:, :, i] + + # Calculate the minimum and maximum pixel values + min_val = np.min(channel) + max_val = np.max(channel) + + # Apply contrast stretching to the channel + stretched_channel = ((channel - min_val) * (255.0 / (max_val - min_val))).astype(np.uint8) + + # Assign the stretched channel to the output image + stretched_image[:, :, i] = stretched_channel + + return stretched_image if __name__ == '__main__': @@ -225,6 +255,8 @@ def FIR_pipeline(img, height, width, ENABLE_IMSHOW=None): # clip the image img = np.clip(255*img, 0, 255).astype(np.uint8) + + ########### post processing ############## diff --git a/python/decode_raw8.py b/python/decode_raw8.py index db114f52..43cb7249 100644 --- a/python/decode_raw8.py +++ b/python/decode_raw8.py @@ -27,7 +27,7 @@ plot_imgs ) -input_name = os.getenv('BINARY_IMG_PATH') or "capture.bin" +input_name = os.getenv('BINARY_IMG_PATH') or "capture.raw" #input_name = Path(__file__).parent / "capture.bin" width, height = 640, 480 diff --git a/python/run_xscope_bin.py b/python/run_xscope_bin.py index 5d423f4e..bc89c997 100644 --- a/python/run_xscope_bin.py +++ b/python/run_xscope_bin.py @@ -92,7 +92,6 @@ def choose_file_with_extension(folder_path, extension): args = parse_arguments() if (args.xe is None): build_folder = Path(__file__).parent.parent.resolve() / "build" - print(build_folder) choose_file_with_extension(build_folder, ".xe") else: run(args.xe) diff --git a/python/test_downs_vs_raw.py b/python/test_downs_vs_raw.py deleted file mode 100644 index cc6e3599..00000000 --- a/python/test_downs_vs_raw.py +++ /dev/null @@ -1,83 +0,0 @@ -import time -import os - -import cv2 -from matplotlib import pyplot as plt -import numpy as np - -from utils import peak_signal_noise_ratio -from PIL import Image -from run_xscope_bin import * -from pathlib import Path - -from FIR_pipeline import FIR_pipeline -from utils import pipeline - - -# path definitions -cwd = str(Path(__file__).parent.resolve()) -top_level = str(Path(__file__).parent.parent.resolve()) -examples = top_level + "/build/examples" - - -def load_image(input_name, height, width, ch=0): - with open(input_name, "rb") as f: - data = f.read() - buffer = np.frombuffer(data, dtype=np.uint8) - if ch == 0: - buffer = buffer.reshape(height, width) - else: - buffer = buffer.reshape(height, width, ch) - return buffer - -if __name__ == '__main__': - ENABLE_IMSHOW = False - RUN_XE = True - kfactor = 4 - - if RUN_XE: - run(examples + '/take_picture_raw/example_take_picture_raw.xe') - time.sleep(1) - run(examples + '/take_picture_downsample/example_take_picture_downsample.xe') - time.sleep(1) - - # base image raw 640x480 - width, height, input_name = 640, 480, 'capture.raw' - img_raw = load_image(input_name, height, width) - - # 1 - raw with decode 8 pipeline - ref_image = pipeline(img_raw, False) - ref_image = cv2.resize(ref_image, (width // kfactor, height // kfactor), interpolation=cv2.INTER_AREA) - - # 2 - raw with simplified FIR pipeline - img_raw_FIR = FIR_pipeline(img_raw, height, width, ENABLE_IMSHOW) - - # 3 - downsampled image in the explorer board - input_name_dwn = 'capture.bin' - width, height = 160, 120 - img_dwn = load_image(input_name_dwn, height, width, 3) - - ############################################## - # Show the 3 imageslist( - fig, axes = plt.subplots(2, 3) - titles = [ - "RAW decode8", - "RAW python FIR", - "Downsampled\nXcore pipeline" - ] - imgs = [ref_image, img_raw_FIR, img_dwn] - - for i, title in enumerate(titles): - axes[0,i].set_title(title) - axes[0,i].imshow(imgs[i]) - axes[1,i].set_ylim([0, 3000]) - axes[1,i].hist(imgs[i].ravel(), bins=256, color='gray', alpha=0.5) - - - plt.subplots_adjust(wspace=0.05) - list(map(lambda axi: axi.set_axis_off(), axes.ravel()[0:3])) - plt.show() - # PSNR - img_psnr = peak_signal_noise_ratio(ref_image, img_raw_FIR) - print(img_psnr) - diff --git a/python/utils.py b/python/utils.py index 24e42d53..a1f22b97 100644 --- a/python/utils.py +++ b/python/utils.py @@ -820,6 +820,32 @@ def pipeline(img, demosaic_opt=True): #it takes a RAW IMAGE # img = run_histogram_equalization(img) return img +def pipeline_raw8(img, demosaic_opt=True): #it takes a RAW IMAGE + as_shot_neutral = [0.6301882863, 1, 0.6555861831] + width, height = 640, 480 + cfa_pattern = [0, 1, 1, 2] # explorer board + + # ------ The ISP pipeline ------------------------- + # black level substraction + img = normalize(img, 15, 254, np.uint8) + # white balancing + img = simple_white_balance(img, as_shot_neutral, cfa_pattern) + # demosaic + img = demosaic(img, cfa_pattern, output_channel_order='RGB', alg_type='VNG') + img_demoisaic = img + # color transforms + img = new_color_correction(img) + # gamma + img = img ** (1.0 / 1.8) + # clip the image + img = np.clip(255*img, 0, 255).astype(np.uint8) + # hist equalization (optional) + # img = run_histogram_equalization(img) + # resize bilinear (optional) + kfactor = 1 + img = cv2.resize(img, (width // kfactor, height // kfactor), interpolation=cv2.INTER_AREA) + # ------ The ISP pipeline ------------------------- + return img def pipeline_nodemosaic(img): @@ -890,5 +916,42 @@ def gray_world(img): # img[:,:,1] = return img + +def iterative_wb(img): + img = img/255.0 + + R = img[:,:,0] + G = img[:,:,1] + B = img[:,:,2] + + # to YUV + a,b,c = 0.299,0.587,0.114 + d,e,f = -0.147,-0.289,0.436 + g,h,i = 0.615,-0.515,-0.100 + + y = a*R + b*G + c*B + u = d*R + e*G + f*B + v = g*R + h*G + i*B + + loc = np.where(y > 0.4) # find high luminance values + + # compute luminance region + yl = y[loc] + ul = u[loc] + vl = v[loc] + + # local to RGB + R = yl + 1.140*vl + G = yl - 0.395*ul - 0.581*vl + B = yl + 2.032*ul + + img[:,:,0] /= R.mean() + img[:,:,1] /= G.mean() + img[:,:,2] /= B.mean() + + img = img.clip(0,1)*255.0 + return img + + if __name__ == '__main__': pass diff --git a/tests/hardware_tests/raw_vs_downsampled/test_downs_vs_raw.py b/tests/hardware_tests/raw_vs_downsampled/test_downs_vs_raw.py new file mode 100644 index 00000000..32fa8668 --- /dev/null +++ b/tests/hardware_tests/raw_vs_downsampled/test_downs_vs_raw.py @@ -0,0 +1,86 @@ +import os +import time +from pathlib import Path +import sys +import cv2 +import numpy as np + +from matplotlib import pyplot as plt +from PIL import Image + +# path definitions +cwd = Path(__file__).parent.resolve() +top_level = str(cwd.parent.parent.parent.resolve()) +examples = top_level + "\\build\\examples" +python_path = top_level + "\\python" + +# python dependencies +sys.path.append(str(python_path)) +from FIR_pipeline import FIR_pipeline_func +from utils import peak_signal_noise_ratio, pipeline_raw8 +from run_xscope_bin import * + +def load_image(input_name, height, width, ch=0): + buffer = np.fromfile(input_name, dtype=np.uint8) + shape = (height, width) if ch == 0 else (height, width, ch) + buffer = buffer.reshape(shape) + return buffer + +if __name__ == '__main__': + ENABLE_IMSHOW = True + RUN_XE = False + + if RUN_XE: + run(examples + '/take_picture_raw/example_take_picture_raw.xe') + time.sleep(1) + run(examples + '/take_picture_downsample/example_take_picture_downsample.xe') + time.sleep(1) + + # base image raw 640x480 + width, height, input_name = 640, 480, 'capture.raw' + img_raw = load_image(input_name, height, width) + + # 1 - raw with decode 8 pipeline + ref_image = pipeline_raw8(img_raw) + ref_image = cv2.resize(ref_image, (width // 4, height // 4), interpolation=cv2.INTER_AREA) + + # 2 - raw with simplified FIR pipeline + img_raw_FIR = FIR_pipeline_func(img_raw, height, width, ENABLE_IMSHOW) + + # 3 - downsampled image in the explorer board + input_name_dwn = 'capture.bin' + width, height = 160, 120 + img_dwn = load_image(input_name_dwn, height, width, 3) + + ############################################## + imgs = [ref_image, img_raw_FIR, img_dwn] + if ENABLE_IMSHOW: + # Show the 3 imageslist( + fig, axes = plt.subplots(2, 3) + titles = [ + "RAW decode8", + "RAW python FIR", + "Downsampled\nXcore pipeline" + ] + def add_caption(ax, caption): + axes[0, i].text(0, -0.1, caption, transform=ax.transAxes, + ha='left', va='top', fontsize=8) + + for i, title in enumerate(titles): + axes[0,i].set_title(title) + axes[0,i].imshow(imgs[i]) + axes[1,i].set_ylim([0, 3000]) + # histogram + for j, col in enumerate(['r', 'g', 'b']): + axes[1,i].hist(imgs[i][:,:,j].ravel(), bins=256, alpha=0.5, color=col) + + # text + img_psnr = peak_signal_noise_ratio(ref_image, imgs[i]) + score = 0 + text = f"SSI: {score:.2f}, \nPSNR: {img_psnr:.2f}" + add_caption(axes[0,i], text) + + plt.subplots_adjust(wspace=0.05) + list(map(lambda axi: axi.set_axis_off(), axes.ravel()[0:3])) + plt.show() + From f45d82895291c194e932dfcb36c2eed6f92c16c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 19 Jun 2023 10:30:05 +0100 Subject: [PATCH 104/306] adding raw vs downsampled exampled and pytest --- python/FIR_pipeline.py | 19 +--- python/utils.py | 11 +++ tests/hardware_tests/pytest.ini | 2 + .../raw_vs_downsampled/test_downs_vs_raw.py | 96 +++++++++++-------- tests/readme.rst | 34 +++++++ 5 files changed, 108 insertions(+), 54 deletions(-) create mode 100644 tests/hardware_tests/pytest.ini create mode 100644 tests/readme.rst diff --git a/python/FIR_pipeline.py b/python/FIR_pipeline.py index a310b08c..111b90ac 100644 --- a/python/FIR_pipeline.py +++ b/python/FIR_pipeline.py @@ -154,10 +154,9 @@ def FIR_pipeline_func(img, height, width, show=False): imgh = horizontal_filer(img, h_filter, height, width) # white balancing - # imgh = gray_world(imgh) + #imgh = gray_world(imgh) imgh = iterative_wb(imgh) - red, green, blue = imgh[:,:,RED], imgh[:,:,GREEN], imgh[:,:,BLUE] # vertical filtering @@ -165,11 +164,11 @@ def FIR_pipeline_func(img, height, width, show=False): ########### post processing ############## # black level substraction - BLACK_LEVEL = 30 + BLACK_LEVEL = 16 img = normalize(img, BLACK_LEVEL, 254, np.uint8) # gamma - # img = img ** (1.0 / 1.8) + img = img ** (1.0 / 1.8) # sharpen (optional) #kernel_sharpen = kernel_sharpen/np.sum(kernel_sharpen) @@ -185,17 +184,7 @@ def FIR_pipeline_func(img, height, width, show=False): # img = stretch_histogram(img) # histeq - img = run_histogram_equalization(img) - - ########################################## - - if show: - plt.imshow(img) - plt.show() - # save image - name = f"out.png" - #print(name) - Image.fromarray(img).save(name) # option 1 pillow + # img = run_histogram_equalization(img) return img diff --git a/python/utils.py b/python/utils.py index a1f22b97..8b05365f 100644 --- a/python/utils.py +++ b/python/utils.py @@ -953,5 +953,16 @@ def iterative_wb(img): return img +def compute_score(img_ref, img): + # if image is color, convert to gray + if img_ref.ndim == 3: + img_ref = cv2.cvtColor(img_ref, cv2.COLOR_RGB2GRAY) + if img.ndim == 3: + img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) + + score = ssim(img_ref, img) + return score + + if __name__ == '__main__': pass diff --git a/tests/hardware_tests/pytest.ini b/tests/hardware_tests/pytest.ini new file mode 100644 index 00000000..e0113cd9 --- /dev/null +++ b/tests/hardware_tests/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +testpaths = "." diff --git a/tests/hardware_tests/raw_vs_downsampled/test_downs_vs_raw.py b/tests/hardware_tests/raw_vs_downsampled/test_downs_vs_raw.py index 32fa8668..702d14d0 100644 --- a/tests/hardware_tests/raw_vs_downsampled/test_downs_vs_raw.py +++ b/tests/hardware_tests/raw_vs_downsampled/test_downs_vs_raw.py @@ -14,10 +14,16 @@ examples = top_level + "\\build\\examples" python_path = top_level + "\\python" +# globals +ENABLE_IMSHOW = True +RUN_XE = False +MIN_PSNR = 15 # DB, peak signal to noise ratio +MIN_SCORE = 0.7 # 70% of similarity + # python dependencies sys.path.append(str(python_path)) from FIR_pipeline import FIR_pipeline_func -from utils import peak_signal_noise_ratio, pipeline_raw8 +from utils import compute_score, peak_signal_noise_ratio, pipeline_raw8 from run_xscope_bin import * def load_image(input_name, height, width, ch=0): @@ -26,61 +32,73 @@ def load_image(input_name, height, width, ch=0): buffer = buffer.reshape(shape) return buffer -if __name__ == '__main__': - ENABLE_IMSHOW = True - RUN_XE = False +def plot_images(imgs, results): + # Show the 3 imageslist( + fig, axes = plt.subplots(2, 3) + titles = [ + "RAW decode8", + "RAW python FIR", + "Downsampled\nXcore pipeline" + ] + def add_caption(ax, caption): + axes[0, i].text(0, -0.1, caption, transform=ax.transAxes, + ha='left', va='top', fontsize=8) + + for i, title in enumerate(titles): + axes[0,i].set_title(title) + axes[0,i].imshow(imgs[i]) + axes[1,i].set_ylim([0, 3000]) + # histogram + for j, col in enumerate(['r', 'g', 'b']): + axes[1,i].hist(imgs[i][:,:,j].ravel(), bins=256, alpha=0.5, color=col) + + # text + score = results[0][i] + img_psnr = results[1][i] + + text = f"SSI: {score:.2f}, \nPSNR: {img_psnr:.2f}" + add_caption(axes[0,i], text) + + plt.subplots_adjust(wspace=0.05) + list(map(lambda axi: axi.set_axis_off(), axes.ravel()[0:3])) + plt.show() + + +def test_downsampled_vs_raw(): if RUN_XE: run(examples + '/take_picture_raw/example_take_picture_raw.xe') time.sleep(1) run(examples + '/take_picture_downsample/example_take_picture_downsample.xe') time.sleep(1) - # base image raw 640x480 + # 0 - base image raw 640x480 width, height, input_name = 640, 480, 'capture.raw' img_raw = load_image(input_name, height, width) - # 1 - raw with decode 8 pipeline ref_image = pipeline_raw8(img_raw) ref_image = cv2.resize(ref_image, (width // 4, height // 4), interpolation=cv2.INTER_AREA) - # 2 - raw with simplified FIR pipeline img_raw_FIR = FIR_pipeline_func(img_raw, height, width, ENABLE_IMSHOW) - # 3 - downsampled image in the explorer board input_name_dwn = 'capture.bin' width, height = 160, 120 img_dwn = load_image(input_name_dwn, height, width, 3) - - ############################################## + # compute results imgs = [ref_image, img_raw_FIR, img_dwn] + img_psnrs = [peak_signal_noise_ratio(ref_image, x) for x in imgs] + scores = [compute_score(ref_image, x) for x in imgs] + results = (img_psnrs, scores) + print(results) + # if we want to plot the images if ENABLE_IMSHOW: - # Show the 3 imageslist( - fig, axes = plt.subplots(2, 3) - titles = [ - "RAW decode8", - "RAW python FIR", - "Downsampled\nXcore pipeline" - ] - def add_caption(ax, caption): - axes[0, i].text(0, -0.1, caption, transform=ax.transAxes, - ha='left', va='top', fontsize=8) - - for i, title in enumerate(titles): - axes[0,i].set_title(title) - axes[0,i].imshow(imgs[i]) - axes[1,i].set_ylim([0, 3000]) - # histogram - for j, col in enumerate(['r', 'g', 'b']): - axes[1,i].hist(imgs[i][:,:,j].ravel(), bins=256, alpha=0.5, color=col) - - # text - img_psnr = peak_signal_noise_ratio(ref_image, imgs[i]) - score = 0 - text = f"SSI: {score:.2f}, \nPSNR: {img_psnr:.2f}" - add_caption(axes[0,i], text) - - plt.subplots_adjust(wspace=0.05) - list(map(lambda axi: axi.set_axis_off(), axes.ravel()[0:3])) - plt.show() - + plot_images(imgs, results) + # assert results are within the expected range + error_msg1 = f"PSNR should be greater than {MIN_PSNR} DB" + error_msg2 = f"SSI should be greater than {MIN_SCORE} %" + assert all(psnr > MIN_PSNR for psnr in img_psnrs), error_msg1+str(img_psnrs) + assert all(score > MIN_SCORE for score in scores), error_msg2+str(scores) + +if __name__ == '__main__': + test_downsampled_vs_raw() + diff --git a/tests/readme.rst b/tests/readme.rst new file mode 100644 index 00000000..55a489de --- /dev/null +++ b/tests/readme.rst @@ -0,0 +1,34 @@ +================================ +Test Folder +================================ + +This folder contains various types of tests for the project. +It contains two types of tests: + 1. Unit tests + 2. Hardware tests + + +Build Tests +============= + +Run the following commands from the top level: + +.. tabs:: + .. tab:: Linux and Mac + + .. code-block:: console + + cmake -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -B build + make -C build tests + + .. tab:: Windows + + .. code-block:: console + + cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -B build + ninja -C build tests + + +Running the tests +============= +* hardware tests require xscope_fileio installed From d3f85aba723fa070545c3618fed4392bfa523d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 19 Jun 2023 15:56:27 +0100 Subject: [PATCH 105/306] adding different AWB algorithms --- camera/api/isp.h | 2 ++ camera/api/statistics.h | 4 +-- camera/src/isp.c | 61 +++++++++++++++++++++++++++++++++++++---- camera/src/statistics.c | 44 +++++++++++++++++++---------- 4 files changed, 89 insertions(+), 22 deletions(-) diff --git a/camera/api/isp.h b/camera/api/isp.h index 74a2eecf..aae31ec6 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -84,6 +84,8 @@ void AWB_compute_gains_white_patch(global_stats_t *gstats, isp_params_t *isp_par void AWB_compute_gains_gray_world(global_stats_t *gstats, isp_params_t *isp_params); +void AWB_compute_gains_white_max(global_stats_t *gstats, isp_params_t *isp_params); + /** * @brief aux function to print the auto white balancing gains * diff --git a/camera/api/statistics.h b/camera/api/statistics.h index 70c7a8dd..90aa6ddc 100644 --- a/camera/api/statistics.h +++ b/camera/api/statistics.h @@ -30,7 +30,7 @@ extern "C" { // fraction. (0.95 will find the value which 95% of pixels are less than or // equal to) #ifndef APP_WB_PERCENTILE -#define APP_WB_PERCENTILE (0.98) +#define APP_WB_PERCENTILE (0.94) #endif // Objects definitions @@ -68,7 +68,7 @@ void compute_skewness(channel_stats_t *stats); */ void compute_simple_stats(channel_stats_t *stats); -void print_simple_stats(channel_stats_t *stats); +void print_simple_stats(channel_stats_t *stats, unsigned channel); /** * Find the value for which (fraction) portion of pixels fall below that value. diff --git a/camera/src/isp.c b/camera/src/isp.c index 539e1ded..7fe54d79 100644 --- a/camera/src/isp.c +++ b/camera/src/isp.c @@ -153,9 +153,9 @@ void AWB_compute_gains_percentile(global_stats_t *gstats, isp_params_t *isp_para gains[i] = m*(sk - skmin) + gmax; } - //tmp0 = 0.7*tmp0 + 0.3*gains[0]; - //tmp1 = 0.7*tmp1 + 0.3*gains[1]; - //tmp2 = 0.7*tmp2 + 0.3*gains[2]; + tmp0 = 0.7*tmp0 + 0.3*gains[0]; + tmp1 = 0.7*tmp1 + 0.3*gains[1]; + tmp2 = 0.7*tmp2 + 0.3*gains[2]; tmp0 = AWB_clip_value(tmp0); tmp1 = AWB_clip_value(tmp1); @@ -190,11 +190,63 @@ void AWB_compute_gains_white_patch(global_stats_t *gstats, isp_params_t *isp_par alfa = AWB_clip_value(alfa); gamma = AWB_clip_value(gamma); + // apply params isp_params->channel_gain[0] = alfa; isp_params->channel_gain[1] = beta; isp_params->channel_gain[2] = gamma; } + +void AWB_compute_gains_white_max(global_stats_t *gstats, isp_params_t *isp_params){ + float alfa = 1.0; + const float beta = 1.0; + float gamma = 1.0; + + uint8_t Rmax = (*gstats)[0].max; // RED + uint8_t Gmax = (*gstats)[1].max; // GREEN + uint8_t Bmax = (*gstats)[2].max; // BLUE + + // correct to have similar white max + uint32_t count_max_red = (*gstats)[0].histogram.bins[Rmax]; + uint32_t count_max_green = (*gstats)[1].histogram.bins[Gmax]; + uint32_t count_max_blue = (*gstats)[2].histogram.bins[Bmax]; + + // false values avoidance + if (count_max_red != 0 && count_max_green != 0 && count_max_blue != 0){ + + if (count_max_green > count_max_red){ + alfa = (count_max_green/(float)count_max_red); + } + + if (count_max_green > count_max_blue){ + gamma = (count_max_green/(float)count_max_blue); + } + } + + // then compute the mean with grey world + float Ravg = (*gstats)[0].mean; // RED + float Gavg = (*gstats)[1].mean; // GREEN + float Bavg = (*gstats)[2].mean; // BLUE + float alfa2 = Gavg/Ravg; + float gamma2 = Gavg/Bavg; + + // armonic mean + float gww = 0.7; // grey world weight + alfa = (1-gww)*alfa + gww*alfa2; + gamma = (1-gww)*gamma + gww*gamma2; + + // clip the values + alfa = AWB_clip_value(alfa); + gamma = AWB_clip_value(gamma); + + // apply params + isp_params->channel_gain[0] = alfa; + isp_params->channel_gain[1] = beta; + isp_params->channel_gain[2] = gamma; +} + + + void AWB_compute_gains_gray_world(global_stats_t *gstats, isp_params_t *isp_params){ printf("AWB --->"); float Ravg = (*gstats)[0].mean; // RED @@ -202,7 +254,7 @@ void AWB_compute_gains_gray_world(global_stats_t *gstats, isp_params_t *isp_para float Bavg = (*gstats)[2].mean; // BLUE float alfa = Gavg/Ravg; - const float beta = 0.9; + const float beta = 1; float gamma = Gavg/Bavg; alfa = AWB_clip_value(alfa); @@ -213,7 +265,6 @@ void AWB_compute_gains_gray_world(global_stats_t *gstats, isp_params_t *isp_para isp_params->channel_gain[2] = gamma; } - void AWB_print_gains(isp_params_t *isp_params){ printf("awb:%f,%f,%f\n", isp_params->channel_gain[0], diff --git a/camera/src/statistics.c b/camera/src/statistics.c index 4bfd0617..b7ae544a 100644 --- a/camera/src/statistics.c +++ b/camera/src/statistics.c @@ -83,12 +83,24 @@ void compute_simple_stats(channel_stats_t *stats) stats->mean = (temp_mean) *(1 << APP_HISTOGRAM_QUANTIZATION_BITS) * histogram_norm_factor; } -void print_simple_stats(channel_stats_t *stats){ - printf("Max: %d\n", stats->max); - printf("Min: %d\n", stats->min); - printf("Mean: %f\n", stats->mean); - printf("Skewness: %f\n", stats->skewness); - printf("Percentile: %d\n", stats->percentile); + +/* +typedef struct { + uint8_t min; + uint8_t max; + uint8_t percentile; + float skewness; + float mean; + channel_histogram_t histogram; +} channel_stats_t; +*/ +void print_simple_stats(channel_stats_t *stats, unsigned channel){ + printf("ch:%d,Min:%d,Max:%d,Mean:%f,Skew:%f\n", + channel, + stats->min, + stats->max, + stats->mean, + stats->skewness); } void find_percentile(channel_stats_t *stats, const float fraction) @@ -140,27 +152,29 @@ void statistics_thread( compute_skewness(&global_stats[channel]); compute_simple_stats(&global_stats[channel]); find_percentile(&global_stats[channel], APP_WB_PERCENTILE); - // print_simple_stats(&global_stats[channel]); + print_simple_stats(&global_stats[channel], channel); } // Adjust AE uint8_t ae_done = AE_control_exposure(&global_stats, sc_if); // Adjust AWB - AWB_compute_gains_static(&global_stats, &isp_params); - if (ae_done == 1){ - //AWB_compute_gains_white_patch(&global_stats, &isp_params); - //AWB_compute_gains_gray_world(&global_stats, &isp_params); - //AWB_compute_gains_percentile(&global_stats, &isp_params); - //AWB_compute_gains_static(&global_stats, &isp_params); + static unsigned run_once = 0; + if (ae_done == 1 && run_once == 0){ + AWB_compute_gains_white_max(&global_stats, &isp_params); //0 + //AWB_compute_gains_white_patch(&global_stats, &isp_params); //1 + //AWB_compute_gains_gray_world(&global_stats, &isp_params); //2 + //AWB_compute_gains_percentile(&global_stats, &isp_params); //3 + //AWB_compute_gains_static(&global_stats, &isp_params); //4 + run_once = 1; // set to 1 to just run once } // Apply gamma curve - //TODO + //TODO here instead of user app // Print ISP info AWB_print_gains(&isp_params); - AE_print_skewness(&global_stats); + //AE_print_skewness(&global_stats); } } From f712d496ed9f92a2c4d14e032dd2bd85ae387c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 19 Jun 2023 16:13:35 +0100 Subject: [PATCH 106/306] gamma same for all colours --- camera/src/isp.c | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/camera/src/isp.c b/camera/src/isp.c index 7fe54d79..63084896 100644 --- a/camera/src/isp.c +++ b/camera/src/isp.c @@ -308,20 +308,6 @@ const uint8_t gamma_1p8_s1[255] = { 235,236,236,237,237,238,238,239,240,240,241,241,242,243,243,244,244,245,245, 246,247,247,247,246,245,244,243,242,241,240,239,238,237,236,235}; -const uint8_t gamma_1p8_s1_green[255] = { -0,12,17,22,25,29,32,35,37,40,42,44,47,49,51,53,55,57,58,60,62,64,65,67,69, -70,72,73,75,76,78,79,80,82,83,85,86,87,89,90,91,92,94,95,96,97,98,100,101, -102,103,104,105,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121, -122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,139, -140,141,142,143,144,145,146,146,147,148,149,150,151,152,152,153,154,155,156, -157,157,158,159,160,161,161,162,163,164,165,165,166,167,168,169,169,170,171, -172,172,173,174,175,175,176,177,178,178,179,180,181,181,182,183,183,184,185, -186,186,187,188,188,189,190,191,191,192,193,193,194,195,195,196,197,198,198, -199,200,200,201,202,202,203,204,204,205,206,206,207,208,208,209,209,210,211, -211,212,213,213,214,215,215,216,217,217,218,218,219,220,220,221,222,222,223, -223,224,225,225,226,226,227,228,228,229,230,230,231,231,232,233,233,234,234, -235,236,236,237,237,238,238,239,240,240,241,241,242,243,243,244,244,245,245, -246,247,247,247,246,245,244,243,242,241,240,239,238,237,236,235}; void isp_gamma_stride1(const uint32_t buffsize, uint8_t *img){ @@ -344,13 +330,7 @@ void isp_gamma_1p8( for(size_t c = 0; c < channels; c++){ size_t index_in = c * (height * width) + k * width + j; uint8_t value = img_in[index_in]; - if (c==1){ - img_in[index_in] = gamma_1p8_s1_green[value]; - } - else{ - img_in[index_in] = gamma_1p8_s1[value]; - } - + img_in[index_in] = gamma_1p8_s1[value]; } } } From f789a3f2f3ea357750be8c1ca679487fc2c70661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 19 Jun 2023 16:23:22 +0100 Subject: [PATCH 107/306] enable to run on device --- tests/hardware_tests/raw_vs_downsampled/test_downs_vs_raw.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/hardware_tests/raw_vs_downsampled/test_downs_vs_raw.py b/tests/hardware_tests/raw_vs_downsampled/test_downs_vs_raw.py index 702d14d0..e6b0945a 100644 --- a/tests/hardware_tests/raw_vs_downsampled/test_downs_vs_raw.py +++ b/tests/hardware_tests/raw_vs_downsampled/test_downs_vs_raw.py @@ -16,8 +16,8 @@ # globals ENABLE_IMSHOW = True -RUN_XE = False -MIN_PSNR = 15 # DB, peak signal to noise ratio +RUN_XE = True +MIN_PSNR = 10 # DB, peak signal to noise ratio MIN_SCORE = 0.7 # 70% of similarity # python dependencies From bf8ae7c1153dd86f9854c431dbdf847c07a47d77 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Mon, 19 Jun 2023 16:26:33 +0100 Subject: [PATCH 108/306] fixing rgb to yuv test --- camera/api/isp.h | 4 +- camera/src/asm/rgb_to_yuv.S | 48 ++++++++------ camera/src/asm/yuv_to_rgb.S | 38 ++++++----- tests/unit_tests/src/test/isp_test.c | 95 ++++++++++++++++------------ 4 files changed, 110 insertions(+), 75 deletions(-) diff --git a/camera/api/isp.h b/camera/api/isp.h index 59b5139e..c442b49a 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -130,9 +130,9 @@ void rotate_image_90(const char *filename, uint8_t image_buffer[APP_IMAGE_CHANNE // -------------------------- COLOR CONVERSION ------------------------------------- // Macro arguments to get color components from packed result in the assembly program -#define GET_R(rgb) ((rgb >> 16) & 0xFF) +#define GET_R(rgb) (rgb & 0xFF) #define GET_G(rgb) ((rgb >> 8) & 0xFF) -#define GET_B(rgb) (rgb & 0xFF) +#define GET_B(rgb) ((rgb >> 16)& 0xFF) #define GET_Y(yuv) GET_R(yuv) #define GET_U(yuv) GET_G(yuv) diff --git a/camera/src/asm/rgb_to_yuv.S b/camera/src/asm/rgb_to_yuv.S index 131f1931..9ab3d407 100644 --- a/camera/src/asm/rgb_to_yuv.S +++ b/camera/src/asm/rgb_to_yuv.S @@ -1,11 +1,11 @@ /* -Converts signed yuv (-127..127, -127..127, -127..127) into signed rgb. +Converts signed rgb (-127..127, -127..127, -127..127) into signed yuv. Returns an int with 3 x int8_t + int rgb_to_yuv( int r, int g, int b); - */ .cc_top rgb_to_yuv.func, rgb_to_yuv @@ -19,44 +19,54 @@ Returns an int with 3 x int8_t #define r r0 #define g r1 #define b r2 +#define _22 r3 +#define nstackwords 8 rgb_to_yuv: -{ ldc r11, 0 ; dualentsp 8 } // Enable dual instructions -{ ldc r3, 22 ; vsetc r11 } // Load 22 in R3 ; control register and the headroom in the vector unit. Signed 32-bit integer -{ shl r, r, r3 ; ldaw r11, sp[0] } // Multiply by 2^22; load word in the Sp -{ shl g, g, r3 ; stw r, r11[0] } // Multiply by 2^22; store y in the vpu -{ shl b, b, r3 ; stw g, r11[1] } // Multiply by 2^22; store u in the vpu +// r4 - r10 are not used here + +{ ldc r11, 0 ; dualentsp nstackwords } // Enable dual instructions +{ ldc _22, 22 ; vsetc r11 } // Load 22 in R3 ; control register and the headroom in the vector unit. Signed 32-bit integer +{ shl r, r, _22 ; ldaw r11, sp[0] } // Multiply by 2^22; load word in the Sp + +{ shl g, g, _22 ; stw r, r11[0] } // Multiply by 2^22; store y in the vpu +{ shl b, b, _22 ; stw g, r11[1] } // Multiply by 2^22; store u in the vpu { ldc r3, 32 ; stw b, r11[2] } // Load constant; store v in the vpu +#undef _22 +#define _32 r3 +#define _24 r1 + #undef y #undef u #undef v -{ ldc r1, 24 ; vclrdr } // load in R1 24 ; Sets the contents of vD and vR in the vector unit to all zeroes. -{ neg r1, r1 ; vldc r11[0] } // signed negation ; load first opdfand +{ ldc _24, 24 ; vclrdr } // load in R1 24 ; Sets the contents of vD and vR in the vector unit to all zeroes. +{ neg _24, _24 ; vldc r11[0] } // signed negation ; load first opdfand ldap r11, Vconv // Load effective address relative to the program counter. -{ add r11, r11, r3 ; vlmaccr r11[0] } -{ add r11, r11, r3 ; vlmaccr r11[0] } +{ add r11, r11, _32 ; vlmaccr r11[0] } +{ add r11, r11, _32 ; vlmaccr r11[0] } { ldaw r11, sp[0] ; vlmaccr r11[0] } { ; vstr r11[0] } - vlashr r11[0], r1 + vlashr r11[0], _24 { ; vdepth8 } { ; vstr r11[0] } { ; ldw r0, sp[0] } -retsp 8 +retsp nstackwords // the values correspond to the matrix values *255 /* -Yconv: .word 157, -132, -26, 0, 0,0,0,0 -Uconv: .word -38, -74, 112, 0, 0,0,0,0 -Vconv: .word 77, 150, 29, 0, 0,0,0,0 +Vconv: .word 157, -132, -26, 0, 0, 0, 0, 0 +Uconv: .word -38, -74, 112, 0, 0, 0, 0, 0 +Yconv: .word 77, 150, 29, 0, 0, 0, 0, 0 */ -Vconv: .word 128, -107, -21, 0, 0,0,0,0 -Uconv: .word -43, -85, 128, 0, 0,0,0,0 -Yconv: .word 77, 150, 29, 0, 0,0,0,0 +Vconv: .word 128, -107, -21, 0, 0, 0, 0, 0 +Uconv: .word -43, -85, 128, 0, 0, 0, 0, 0 +Yconv: .word 77, 150, 29, 0, 0, 0, 0, 0 + .cc_bottom rgb_to_yuv.func diff --git a/camera/src/asm/yuv_to_rgb.S b/camera/src/asm/yuv_to_rgb.S index 939259ae..e9e23e14 100644 --- a/camera/src/asm/yuv_to_rgb.S +++ b/camera/src/asm/yuv_to_rgb.S @@ -6,7 +6,6 @@ Returns an int with 3 x int8_t int y, int u, int v); - */ .cc_top yuv_to_rgb.func, yuv_to_rgb @@ -20,38 +19,47 @@ Returns an int with 3 x int8_t #define y r0 #define u r1 #define v r2 +#define _22 r3 +#define nstackwords 8 yuv_to_rgb: -{ ldc r11, 0 ; dualentsp 8 } // Enable dual instructions -{ ldc r3, 22 ; vsetc r11 } // Load 22 in R3 ; control register and the headroom in the vector unit. Signed 32-bit integer -{ shl y, y, r3 ; ldaw r11, sp[0] } // Multiply by 2^22; load word in the Sp -{ shl u, u, r3 ; stw y, r11[0] } // Multiply by 2^22; store y in the vpu -{ shl v, v, r3 ; stw u, r11[1] } // Multiply by 2^22; store u in the vpu +// r4 - r10 are not used here + +{ ldc r11, 0 ; dualentsp nstackwords } // Enable dual instructions +{ ldc _22, 22 ; vsetc r11 } // Load 22 in R3 ; control register and the headroom in the vector unit. Signed 32-bit integer +{ shl y, y, _22 ; ldaw r11, sp[0] } // Multiply by 2^22; load word in the Sp + +{ shl u, u, _22 ; stw y, r11[0] } // Multiply by 2^22; store y in the vpu +{ shl v, v, _22 ; stw u, r11[1] } // Multiply by 2^22; store u in the vpu { ldc r3, 32 ; stw v, r11[2] } // Load constant; store v in the vpu +#undef _22 +#define _32 r3 +#define _24 r1 + #undef y #undef u #undef v -{ ldc r1, 24 ; vclrdr } // load in R1 24 ; Sets the contents of vD and vR in the vector unit to all zeroes. -{ neg r1, r1 ; vldc r11[0] } // signed negation ; load first opdfand +{ ldc _24, 24 ; vclrdr } // load in R1 24 ; Sets the contents of vD and vR in the vector unit to all zeroes. +{ neg _24, _24 ; vldc r11[0] } // signed negation ; load first opdfand ldap r11, bluConv // Load effective address relative to the program counter. -{ add r11, r11, r3 ; vlmaccr r11[0] } -{ add r11, r11, r3 ; vlmaccr r11[0] } +{ add r11, r11, _32 ; vlmaccr r11[0] } +{ add r11, r11, _32 ; vlmaccr r11[0] } { ldaw r11, sp[0] ; vlmaccr r11[0] } { ; vstr r11[0] } - vlashr r11[0], r1 + vlashr r11[0], _24 { ; vdepth8 } { ; vstr r11[0] } { ; ldw r0, sp[0] } -retsp 8 +retsp nstackwords // the values correspond to the matrix values *255 // https://en.wikipedia.org/wiki/YUV -bluConv: .word 256, 520, 0, 0, 0,0,0,0 -grnConv: .word 256, -101, -149, 0, 0,0,0,0 -redConv: .word 256, 0, 292, 0, 0,0,0,0 +bluConv: .word 256, 520, 0, 0, 0, 0, 0, 0 +grnConv: .word 256, -101, -149, 0, 0, 0, 0, 0 +redConv: .word 256, 0, 292, 0, 0, 0, 0, 0 .cc_bottom yuv_to_rgb.func diff --git a/tests/unit_tests/src/test/isp_test.c b/tests/unit_tests/src/test/isp_test.c index 507fed19..e86b62b4 100644 --- a/tests/unit_tests/src/test/isp_test.c +++ b/tests/unit_tests/src/test/isp_test.c @@ -22,7 +22,7 @@ TEST_SETUP(isp_tests) { fflush(stdout); } TEST_TEAR_DOWN(isp_tests) {} -#define INV_DELTA 30 // error allowed in YUV RGB color conversion +#define INV_DELTA 20 // error allowed in YUV RGB color conversion #define CT_INT 127 // int conversion // Store the RGB color and corresponding values @@ -32,15 +32,24 @@ typedef struct int Y, U, V; } color_table_t; - -void printColorTable(color_table_t* table) { +void printColorTable(color_table_t* table, uint8_t ref) { + if(ref) + { + printf("Expected "); + } + else + { + printf("Resulted "); + } printf("Color Table:\n"); printf("R: %d, G: %d, B: %d\n", table->R, table->G, table->B); printf("Y: %d, U: %d, V: %d\n", table->Y, table->U, table->V); } void yuv_to_rgb_ct(color_table_t* ct_ref, color_table_t* ct_res){ - *ct_res = *ct_ref; + ct_res -> R = ct_ref -> R; + ct_res -> G = ct_ref -> G; + ct_res -> B = ct_ref -> B; uint32_t result = yuv_to_rgb(ct_ref->Y - CT_INT, ct_ref->U - CT_INT, ct_ref->V - CT_INT); ct_res -> R = (uint8_t)(GET_R(result) + CT_INT); ct_res -> G = (uint8_t)(GET_G(result) + CT_INT); @@ -48,51 +57,59 @@ void yuv_to_rgb_ct(color_table_t* ct_ref, color_table_t* ct_res){ } void rgb_to_yuv_ct(color_table_t* ct_ref, color_table_t* ct_res){ - *ct_res = *ct_ref; - uint32_t result = rgb_to_yuv(ct_ref->Y - CT_INT, ct_ref->U - CT_INT, ct_ref->V - CT_INT); + uint32_t result = rgb_to_yuv(ct_ref->R - CT_INT, ct_ref->G - CT_INT, ct_ref->B - CT_INT); ct_res -> Y = (uint8_t)(GET_Y(result) + CT_INT); ct_res -> U = (uint8_t)(GET_U(result) + CT_INT); ct_res -> V = (uint8_t)(GET_V(result) + CT_INT); } - - -color_table_t ct_test = {16, 100, 16, 65, 100, 92}; // R G B Y U V -//TODO include more colors for testing +// R G B Y U V +#define num_tests 3 +color_table_t ct_test_vector[num_tests] = { + {48, 100, 16, 74, 94, 108}, + {192, 70, 23, 101, 83, 192}, + {58, 3, 156, 36, 195, 143} +}; +//TODO randomize this data TEST(isp_tests, yuv_to_rgb) { - // Define color table - color_table_t ct_ref = ct_test; - color_table_t ct_result; - - // Test the converison - yuv_to_rgb_ct(&ct_ref, &ct_result); - - printColorTable(&ct_test); - printColorTable(&ct_result); - - // Ensure conversion is correct - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.R, ct_result.R); - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.G, ct_result.G); - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.B, ct_result.B); + for(size_t i = 0; i < num_tests; i++) + { + // Define color table + color_table_t ct_ref = ct_test_vector[i]; + color_table_t ct_result; + + // Test the converison + yuv_to_rgb_ct(&ct_ref, &ct_result); + + printColorTable(&ct_ref, 1); + printColorTable(ct_result, 0); + + // Ensure conversion is correct + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.R, ct_result->R); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.G, ct_result->G); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.B, ct_result->B); + } } - TEST(isp_tests, rgb_to_yuv) { - // Define color table - color_table_t ct_ref = ct_test; - color_table_t ct_result; - - // Test the converison - rgb_to_yuv_ct(&ct_ref, &ct_result); - - printColorTable(&ct_test); - printColorTable(&ct_result); - - // Printing the extracted bytes - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.Y, ct_result.Y); - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.U, ct_result.U); - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.V, ct_result.V); + for(size_t i = 0; i < num_tests; i++) + { + // Define color table + color_table_t ct_ref = ct_test_vector[i]; + color_table_t * ct_result = &ct_ref; + + // Test the converison + rgb_to_yuv_ct(&ct_ref, ct_result); + + printColorTable(&ct_ref, 1); + printColorTable(ct_result, 0); + + // Printing the extracted bytes + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.Y, ct_result->Y); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.U, ct_result->U); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.V, ct_result->V); + } } From 1166f9d7d9eecf2ee89902054ab199f82c1000fa Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Mon, 19 Jun 2023 16:57:19 +0100 Subject: [PATCH 109/306] review request --- tests/unit_tests/src/test/isp_test.c | 31 +++++++++++++++------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/tests/unit_tests/src/test/isp_test.c b/tests/unit_tests/src/test/isp_test.c index e86b62b4..849daff1 100644 --- a/tests/unit_tests/src/test/isp_test.c +++ b/tests/unit_tests/src/test/isp_test.c @@ -47,9 +47,9 @@ void printColorTable(color_table_t* table, uint8_t ref) { } void yuv_to_rgb_ct(color_table_t* ct_ref, color_table_t* ct_res){ - ct_res -> R = ct_ref -> R; - ct_res -> G = ct_ref -> G; - ct_res -> B = ct_ref -> B; + ct_res -> Y = ct_ref -> Y; + ct_res -> U = ct_ref -> U; + ct_res -> V = ct_ref -> V; uint32_t result = yuv_to_rgb(ct_ref->Y - CT_INT, ct_ref->U - CT_INT, ct_ref->V - CT_INT); ct_res -> R = (uint8_t)(GET_R(result) + CT_INT); ct_res -> G = (uint8_t)(GET_G(result) + CT_INT); @@ -57,6 +57,9 @@ void yuv_to_rgb_ct(color_table_t* ct_ref, color_table_t* ct_res){ } void rgb_to_yuv_ct(color_table_t* ct_ref, color_table_t* ct_res){ + ct_res -> R = ct_ref -> R; + ct_res -> G = ct_ref -> G; + ct_res -> B = ct_ref -> B; uint32_t result = rgb_to_yuv(ct_ref->R - CT_INT, ct_ref->G - CT_INT, ct_ref->B - CT_INT); ct_res -> Y = (uint8_t)(GET_Y(result) + CT_INT); ct_res -> U = (uint8_t)(GET_U(result) + CT_INT); @@ -78,18 +81,18 @@ TEST(isp_tests, yuv_to_rgb) { // Define color table color_table_t ct_ref = ct_test_vector[i]; - color_table_t ct_result; + color_table_t ct_result = {0}; // Test the converison yuv_to_rgb_ct(&ct_ref, &ct_result); printColorTable(&ct_ref, 1); - printColorTable(ct_result, 0); + printColorTable(&ct_result, 0); // Ensure conversion is correct - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.R, ct_result->R); - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.G, ct_result->G); - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.B, ct_result->B); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.R, ct_result.R); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.G, ct_result.G); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.B, ct_result.B); } } @@ -99,17 +102,17 @@ TEST(isp_tests, rgb_to_yuv) { // Define color table color_table_t ct_ref = ct_test_vector[i]; - color_table_t * ct_result = &ct_ref; + color_table_t ct_result = {0}; // Test the converison - rgb_to_yuv_ct(&ct_ref, ct_result); + rgb_to_yuv_ct(&ct_ref, &ct_result); printColorTable(&ct_ref, 1); - printColorTable(ct_result, 0); + printColorTable(&ct_result, 0); // Printing the extracted bytes - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.Y, ct_result->Y); - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.U, ct_result->U); - TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.V, ct_result->V); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.Y, ct_result.Y); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.U, ct_result.U); + TEST_ASSERT_INT_WITHIN(INV_DELTA, ct_ref.V, ct_result.V); } } From 48ab21830d0dd8548b79c6f388e25e6640a9745b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 19 Jun 2023 17:02:44 +0100 Subject: [PATCH 110/306] review corrections --- camera/src/statistics.c | 1 - examples/take_picture_downsample/src/app.c | 4 +- python/compare_images.py | 69 ---------------------- python/run_xscope_bin.py | 2 +- 4 files changed, 3 insertions(+), 73 deletions(-) delete mode 100644 python/compare_images.py diff --git a/camera/src/statistics.c b/camera/src/statistics.c index a3d3d30f..e5df1aa7 100644 --- a/camera/src/statistics.c +++ b/camera/src/statistics.c @@ -183,7 +183,6 @@ void statistics_thread( // Print ISP info AWB_print_gains(&isp_params); - //AE_print_skewness(&global_stats); } } diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index 2ed7429f..3e2a6eb6 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -5,9 +5,9 @@ #include "io_utils.h" #include "app.h" -//#include "isp.h" +#include "isp.h" // needed for gamma -#define APPLY_GAMMA 0 +#define APPLY_GAMMA 1 void user_app() { diff --git a/python/compare_images.py b/python/compare_images.py deleted file mode 100644 index 8701a2bb..00000000 --- a/python/compare_images.py +++ /dev/null @@ -1,69 +0,0 @@ -import time -import os - -import cv2 -from matplotlib import pyplot as plt -import numpy as np - -from utils import peak_signal_noise_ratio -from PIL import Image -from run_xscope_bin import * -from pathlib import Path - -from FIR_pipeline import FIR_pipeline -from utils import pipeline - - -# path definitions -cwd = str(Path(__file__).parent.resolve()) -top_level = str(Path(__file__).parent.parent.resolve()) -examples = top_level + "/build/examples" - - -def load_image(input_name, height, width, ch=0): - with open(input_name, "rb") as f: - data = f.read() - buffer = np.frombuffer(data, dtype=np.uint8) - if ch == 0: - buffer = buffer.reshape(height, width) - else: - buffer = buffer.reshape(height, width, ch) - return buffer - -if __name__ == '__main__': - ENABLE_IMSHOW = 1 - kfactor = 4 - - run(examples + '/take_picture_raw/example_take_picture_raw.xe') - time.sleep(1) - run(examples + '/take_picture_downsample/example_take_picture_downsample.xe') - time.sleep(1) - - # load - width, height, input_name = 640, 480, 'capture.raw' - img_raw = load_image(input_name, height, width) - - # highest quality - ref_image = pipeline(img_raw, False) - ref_image = cv2.resize(ref_image, (width // kfactor, height // kfactor), interpolation=cv2.INTER_AREA) - - # raw with python pipeline - img_raw_FIR = FIR_pipeline(img_raw, height, width, ENABLE_IMSHOW) - - # downsampled - input_name_dwn = 'capture.bin' - width, height = 160, 120 - img_dwn = load_image(input_name_dwn, height, width, 3) - - # show the 3 images - plt.subplot(1, 3, 1) - plt.imshow(ref_image) - plt.subplot(1, 3, 2) - plt.imshow(img_raw_FIR) - plt.subplot(1, 3, 3) - plt.imshow(img_dwn) - - # PSNR - img_psnr = peak_signal_noise_ratio(ref_image, img_raw_FIR) - print(img_psnr) - diff --git a/python/run_xscope_bin.py b/python/run_xscope_bin.py index bc89c997..d53da462 100644 --- a/python/run_xscope_bin.py +++ b/python/run_xscope_bin.py @@ -15,7 +15,7 @@ def get_adapter_id(): print('Error: %s' % e.output) assert False except FileNotFoundError: - msg = ("please ensure you have xcc tools activated in your environment") + msg = ("please ensure you have XMOS tools activated in your environment") assert False, msg xrun_out = xrun_out.split('\n') From 4e176af298932e7f74e1a6fe579518f22eb1dc26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 19 Jun 2023 17:58:23 +0100 Subject: [PATCH 111/306] introduction finished --- doc/1_programming_guide/01_Introduction.rst | 62 +++++++++++------- .../images/1_high_level_view.png | Bin 0 -> 53162 bytes .../images/2_functional_object.png | Bin 0 -> 258737 bytes .../images/image_names.rst | 3 + .../images/overviewdrawio.png | Bin 99008 -> 0 bytes 5 files changed, 41 insertions(+), 24 deletions(-) create mode 100644 doc/1_programming_guide/images/1_high_level_view.png create mode 100644 doc/1_programming_guide/images/2_functional_object.png create mode 100644 doc/1_programming_guide/images/image_names.rst delete mode 100644 doc/1_programming_guide/images/overviewdrawio.png diff --git a/doc/1_programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst index 426c4fd9..3c15696e 100644 --- a/doc/1_programming_guide/01_Introduction.rst +++ b/doc/1_programming_guide/01_Introduction.rst @@ -1,47 +1,61 @@ Introduction ============= +.. include:: images/image_names.rst .. contents:: Table of Contents Overview --------- -The purpose of this programming guide is to provide developers with a comprehensive understanding of the FWK_Camera architecture and guide them on how to effectively interact with camera using xmos devices. +The purpose of this programming guide is to provide developers with a comprehensive understanding of the FWK_Camera architecture and guide them on how to effectively interact with cameras using XMOS devices. -The architecture consists of several key components that work together to facilitate camera communication and data processing. -These components include: +The architecture consists of several key components that work together to facilitate camera communication and data processing. These components include: -#. Camera hardware and camera interface -#. Camera drivers -#. User aspplication / user interface -#. Image signal processing -#. I/O +- Camera hardware and camera interface +- Camera drivers +- User application / user interface +- Image signal processing +- I/O -[INSERT HERE HIGH LEVEL DIAGRAM] + .. figure:: images/{figure1} + :alt: High Level Block Diagram + :figwidth: 400px + High Level Block Diagram Conventions and Terminology --------------------------- -(MIPI definitions and specs) - -(xcore spifications channends, for example) - -(I2C control, ) +To ensure clarity and consistency throughout this guide, the following conventions and terminology are used: +- MIPI: Mobile Industry Processor Interface. It is a standard interface specification for mobile devices, including cameras. The FWK_Camera architecture utilizes MIPI specifications for camera communication. +- Xcore: XMOS proprietary event-driven processor architecture. It provides high-performance parallel processing capabilities and is used in XMOS devices to handle camera interface and data processing. +- Channels: In the context of XMOS devices, channels are communication pathways that allow data exchange between different components. Channels play a crucial role in camera control and data transfer within the FWK_Camera architecture. +- I2C: Inter-Integrated Circuit. It is a widely used serial communication protocol for controlling and configuring devices. Within the FWK_Camera architecture, I2C is utilized for camera control operations, such as adjusting settings and retrieving sensor information. Features --------- -* MIPI CSI2 interface -* Up to 1GBps per lane -* Low resolution filtering -* Cameras supported: - * IMX219 - * GC2145 (explain hw modification) +- MIPI CSI2 interface +- Up to 1GBps per lane +- Low resolution filtering +- Cameras supported: + - IMX219 + - GC2145 (explain hardware modification) Getting Started ---------------- - +Hardware requirements: +- XMOS device +- Camera module +- Power supply +- USB cable +- JTAG debugger + +Software requirements: +- XMOS tools: https://www.xmos.ai/software-tools/ +- FWK_Camera repository +- CMake, Ninja (Windows) Additional Resources --------------------- -(MIPi-csi specs) -(mipi xcore reference) -(camera datahseets) +- MIPI CSI-2 specification: https://www.mipi.org/specifications/csi-2 +- XMOS I2C library user guide: [Link to user guide](https://www.xmos.ai/download/lib_i2c-%5Buserguide%5D(5.0.0rc3).pdf) +- XMOS Programming Guide: [Link to programming guide](https://www.xmos.ai/download/XMOS-Programming-Guide-(documentation)(E).pdf) +- IMX219 datasheet: [Link to datasheet](https://www.opensourceinstruments.com/Electronics/Data/IMX219PQ.pdf) diff --git a/doc/1_programming_guide/images/1_high_level_view.png b/doc/1_programming_guide/images/1_high_level_view.png new file mode 100644 index 0000000000000000000000000000000000000000..cdce0c9e4ee20eb72ca6bfa836423d97e4b994d2 GIT binary patch literal 53162 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 literal 0 HcmV?d00001 diff --git a/doc/1_programming_guide/images/2_functional_object.png b/doc/1_programming_guide/images/2_functional_object.png new file mode 100644 index 0000000000000000000000000000000000000000..e5eab3fe6efb9dcce10586ae1f61b07609215fa4 GIT binary patch literal 258737 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 q4lUKa;^>q) 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 F{sT`Rs>L@7a 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

q+mZfx9p3-@>p1Vj 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 QAAo-+RdrQLm2ATQ3!$l?r2qf` From 077a098224bae302fa6beadbd2f516c5e7d3df59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 19 Jun 2023 18:02:44 +0100 Subject: [PATCH 112/306] xscope --- utils/xscope_fileio | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/xscope_fileio b/utils/xscope_fileio index cca94745..63bce94d 160000 --- a/utils/xscope_fileio +++ b/utils/xscope_fileio @@ -1 +1 @@ -Subproject commit cca94745b63b3f7bbc93fc34f2111a196a8937df +Subproject commit 63bce94d5e80a294e8fc38d1482913af5bac2a14 From 99d859fede30c58db9ebd9cb52996b6d0e17b095 Mon Sep 17 00:00:00 2001 From: xalbertoisorna <124703429+xalbertoisorna@users.noreply.github.com> Date: Mon, 19 Jun 2023 18:05:47 +0100 Subject: [PATCH 113/306] Update 01_Introduction.rst --- doc/1_programming_guide/01_Introduction.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/1_programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst index 3c15696e..ad0cd4cb 100644 --- a/doc/1_programming_guide/01_Introduction.rst +++ b/doc/1_programming_guide/01_Introduction.rst @@ -42,6 +42,7 @@ Features Getting Started ---------------- Hardware requirements: + - XMOS device - Camera module - Power supply @@ -49,6 +50,7 @@ Hardware requirements: - JTAG debugger Software requirements: + - XMOS tools: https://www.xmos.ai/software-tools/ - FWK_Camera repository - CMake, Ninja (Windows) From a5a14bdb2e03e76a612c11d1bce3f003db68f884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 19 Jun 2023 18:13:15 +0100 Subject: [PATCH 114/306] xmos links --- doc/1_programming_guide/01_Introduction.rst | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/doc/1_programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst index 3c15696e..4ae926b2 100644 --- a/doc/1_programming_guide/01_Introduction.rst +++ b/doc/1_programming_guide/01_Introduction.rst @@ -55,7 +55,13 @@ Software requirements: Additional Resources --------------------- -- MIPI CSI-2 specification: https://www.mipi.org/specifications/csi-2 -- XMOS I2C library user guide: [Link to user guide](https://www.xmos.ai/download/lib_i2c-%5Buserguide%5D(5.0.0rc3).pdf) -- XMOS Programming Guide: [Link to programming guide](https://www.xmos.ai/download/XMOS-Programming-Guide-(documentation)(E).pdf) -- IMX219 datasheet: [Link to datasheet](https://www.opensourceinstruments.com/Electronics/Data/IMX219PQ.pdf) +.. _MIPI: https://www.mipi.org/specifications/csi-2 +.. _XMOS: https://www.xmos.ai/ +.. _XMOSI2C: https://www.xmos.ai/download/lib_i2c-%5Buserguide%5D(5.0.0rc3).pdf +.. _XMOSProgrammingGuide: https://www.xmos.ai/download/XMOS-Programming-Guide-(documentation)(E).pdf +.. _IMX219: https://www.opensourceinstruments.com/Electronics/Data/IMX219PQ.pdf + +- MIPI CSI-2 specification: `MIPI`_. +- XMOS I2C library user guide: `XMOSI2C`_. +- XMOS Programming Guide: `XMOSProgrammingGuide`_. +- IMX219 datasheet: `IMX219`_. From 6789f4dd322765cf5afeb250c623a71f8cfd2d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 19 Jun 2023 18:19:14 +0100 Subject: [PATCH 115/306] adding figure --- doc/1_programming_guide/01_Introduction.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/1_programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst index bc731d78..ba8480d8 100644 --- a/doc/1_programming_guide/01_Introduction.rst +++ b/doc/1_programming_guide/01_Introduction.rst @@ -63,7 +63,7 @@ Additional Resources .. _XMOSProgrammingGuide: https://www.xmos.ai/download/XMOS-Programming-Guide-(documentation)(E).pdf .. _IMX219: https://www.opensourceinstruments.com/Electronics/Data/IMX219PQ.pdf -- MIPI CSI-2 specification: `MIPI`_. -- XMOS I2C library user guide: `XMOSI2C`_. -- XMOS Programming Guide: `XMOSProgrammingGuide`_. -- IMX219 datasheet: `IMX219`_. +- MIPI CSI-2 specification: `MIPI`_ +- XMOS I2C library user guide: `XMOSI2C`_ +- XMOS Programming Guide: `XMOSProgrammingGuide`_ +- IMX219 datasheet: `IMX219`_ From aac4fdb1d69ccdefe3165bac7f0be09ba3abd5c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 19 Jun 2023 18:19:25 +0100 Subject: [PATCH 116/306] adding figure --- doc/1_programming_guide/01_Introduction.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/1_programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst index ba8480d8..db36d80c 100644 --- a/doc/1_programming_guide/01_Introduction.rst +++ b/doc/1_programming_guide/01_Introduction.rst @@ -16,10 +16,10 @@ The architecture consists of several key components that work together to facili - Image signal processing - I/O - .. figure:: images/{figure1} - :alt: High Level Block Diagram - :figwidth: 400px - High Level Block Diagram +.. figure:: images/{figure1} + :alt: High Level Block Diagram + :figwidth: 400px + High Level Block Diagram Conventions and Terminology --------------------------- From fe6def9878618adfeea491c6fb7dcc7cb690341c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 19 Jun 2023 18:22:07 +0100 Subject: [PATCH 117/306] figure 2 --- doc/1_programming_guide/01_Introduction.rst | 2 +- doc/1_programming_guide/02_Architecture_and_Design.rst | 2 +- doc/1_programming_guide/images/image_names.rst | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/doc/1_programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst index db36d80c..d23ed103 100644 --- a/doc/1_programming_guide/01_Introduction.rst +++ b/doc/1_programming_guide/01_Introduction.rst @@ -16,7 +16,7 @@ The architecture consists of several key components that work together to facili - Image signal processing - I/O -.. figure:: images/{figure1} +.. figure:: images/|figure1| :alt: High Level Block Diagram :figwidth: 400px High Level Block Diagram diff --git a/doc/1_programming_guide/02_Architecture_and_Design.rst b/doc/1_programming_guide/02_Architecture_and_Design.rst index da8ee871..6670d5ec 100644 --- a/doc/1_programming_guide/02_Architecture_and_Design.rst +++ b/doc/1_programming_guide/02_Architecture_and_Design.rst @@ -9,7 +9,7 @@ Introduction System Architecture -------------------- -.. figure:: images/overviewdrawio.png +.. figure:: images/|figure2| :alt: Alternative Text :figwidth: 400px :figclass: custom-class diff --git a/doc/1_programming_guide/images/image_names.rst b/doc/1_programming_guide/images/image_names.rst index 20a5685b..01f061e5 100644 --- a/doc/1_programming_guide/images/image_names.rst +++ b/doc/1_programming_guide/images/image_names.rst @@ -1,3 +1,2 @@ -:figure1: 1_high_level_view.png -:figure2: 2_functional_object.png -:figure3: myimage3.png +.. |figure1| replace:: 1_high_level_view.png +.. |figure2| replace:: 2_functional_object.png From 9584fafc8e2f5ce42f57f3f3a456d65b7575b661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 19 Jun 2023 18:25:27 +0100 Subject: [PATCH 118/306] rst links --- doc/1_programming_guide/02_Architecture_and_Design.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/1_programming_guide/02_Architecture_and_Design.rst b/doc/1_programming_guide/02_Architecture_and_Design.rst index 6670d5ec..cfb4067e 100644 --- a/doc/1_programming_guide/02_Architecture_and_Design.rst +++ b/doc/1_programming_guide/02_Architecture_and_Design.rst @@ -1,6 +1,7 @@ Architecture and Design ======================= +.. include:: images/image_names.rst .. contents:: Table of Contents Introduction From 545215c8298d6cf18c720b009c4727e6a9c3852a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Tue, 20 Jun 2023 09:16:09 +0100 Subject: [PATCH 119/306] solving images issue --- doc/1_programming_guide/01_Introduction.rst | 12 ++++++++---- .../02_Architecture_and_Design.rst | 4 ++-- doc/1_programming_guide/images/image_names.rst | 2 -- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/doc/1_programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst index d23ed103..9df8c660 100644 --- a/doc/1_programming_guide/01_Introduction.rst +++ b/doc/1_programming_guide/01_Introduction.rst @@ -16,10 +16,14 @@ The architecture consists of several key components that work together to facili - Image signal processing - I/O -.. figure:: images/|figure1| - :alt: High Level Block Diagram - :figwidth: 400px - High Level Block Diagram +.. figure:: images/1_high_level_view.png + :alt: Alternate text for the image + :width: 400px + :align: center + + Caption for the image goes here. + + Conventions and Terminology --------------------------- diff --git a/doc/1_programming_guide/02_Architecture_and_Design.rst b/doc/1_programming_guide/02_Architecture_and_Design.rst index cfb4067e..a8700d4f 100644 --- a/doc/1_programming_guide/02_Architecture_and_Design.rst +++ b/doc/1_programming_guide/02_Architecture_and_Design.rst @@ -10,12 +10,12 @@ Introduction System Architecture -------------------- -.. figure:: images/|figure2| +.. figure:: images/2_functional_object.png :alt: Alternative Text :figwidth: 400px :figclass: custom-class - Caption for the Figure. + Class Diagram diff --git a/doc/1_programming_guide/images/image_names.rst b/doc/1_programming_guide/images/image_names.rst index 01f061e5..e69de29b 100644 --- a/doc/1_programming_guide/images/image_names.rst +++ b/doc/1_programming_guide/images/image_names.rst @@ -1,2 +0,0 @@ -.. |figure1| replace:: 1_high_level_view.png -.. |figure2| replace:: 2_functional_object.png From 9162efdb1484e8439bfe7e306791ce33e56ee0ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Tue, 20 Jun 2023 11:31:14 +0100 Subject: [PATCH 120/306] review corrections --- doc/1_programming_guide/01_Introduction.rst | 9 ++++----- doc/1_programming_guide/02_Architecture_and_Design.rst | 1 - doc/1_programming_guide/images/image_names.rst | 0 3 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 doc/1_programming_guide/images/image_names.rst diff --git a/doc/1_programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst index 9df8c660..95532f2c 100644 --- a/doc/1_programming_guide/01_Introduction.rst +++ b/doc/1_programming_guide/01_Introduction.rst @@ -1,12 +1,11 @@ Introduction ============= -.. include:: images/image_names.rst .. contents:: Table of Contents Overview --------- -The purpose of this programming guide is to provide developers with a comprehensive understanding of the FWK_Camera architecture and guide them on how to effectively interact with cameras using XMOS devices. +The purpose of this programming guide is to provide developers with a comprehensive understanding of the FWK_Camera architecture and guide them on how to effectively interact with cameras using the XCORE-AI-EXPLORER board. The architecture consists of several key components that work together to facilitate camera communication and data processing. These components include: @@ -21,7 +20,7 @@ The architecture consists of several key components that work together to facili :width: 400px :align: center - Caption for the image goes here. + High level block diagram of fwk camera. @@ -47,10 +46,10 @@ Getting Started ---------------- Hardware requirements: -- XMOS device +- XCORE.AI EVALUATION KIT (XK-EVK-XU316) - Camera module - Power supply -- USB cable +- Micro USB cable - JTAG debugger Software requirements: diff --git a/doc/1_programming_guide/02_Architecture_and_Design.rst b/doc/1_programming_guide/02_Architecture_and_Design.rst index a8700d4f..6ac640a1 100644 --- a/doc/1_programming_guide/02_Architecture_and_Design.rst +++ b/doc/1_programming_guide/02_Architecture_and_Design.rst @@ -1,7 +1,6 @@ Architecture and Design ======================= -.. include:: images/image_names.rst .. contents:: Table of Contents Introduction diff --git a/doc/1_programming_guide/images/image_names.rst b/doc/1_programming_guide/images/image_names.rst deleted file mode 100644 index e69de29b..00000000 From f2d3f32ca7d8c2be7869e190993a3febe6446f50 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 20 Jun 2023 15:00:13 +0100 Subject: [PATCH 121/306] moving black level to sensor.h, fixing hfilter test --- camera/api/isp.h | 3 -- camera/src/image_hfilter.c | 2 +- sensors/api/sensor.h | 4 +- .../unit_tests/src/test/pixel_hfilter_test.c | 48 +++++++++++-------- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/camera/api/isp.h b/camera/api/isp.h index 4ca13216..3d1eaea8 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -7,9 +7,6 @@ #include "statistics.h" -// black level is sensor dependant (used by horizontal filter) -#define BLACK_LEVEL 16 - // ---------------------------------- AE/AGC ------------------------------ /** diff --git a/camera/src/image_hfilter.c b/camera/src/image_hfilter.c index e120fe46..bfba2f8a 100644 --- a/camera/src/image_hfilter.c +++ b/camera/src/image_hfilter.c @@ -40,5 +40,5 @@ void pixel_hfilter_update_scale( const float sum_b = b0 + 2*b1; - state->acc_init = (128 * (sum_b - shift_scale - BLACK_LEVEL)); + state->acc_init = 128 * (sum_b - shift_scale) - SENSOR_BLACK_LEVEL * shift_scale; } diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index 2dd1aeed..8de5a3a7 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -33,7 +33,7 @@ #define camera_stop(iic) imx219_stream_stop(iic) #define camera_configure(iic) imx219_configure_mode(iic) #define camera_set_exposure(iic,ex) imx219_set_gain_dB(iic,ex) - + #define SENSOR_BLACK_LEVEL 16 #endif #if CONFIG_GC2145_SUPPORT @@ -44,10 +44,10 @@ #define camera_stop(iic) gcstop(iic) #define camera_configure(iic) gcconfigure(iic) #define camera_set_exposure(iic,ex) gcsetexp(iic,ex) + #define SENSOR_BLACK_LEVEL 0 */ #endif - // Modes configurations #ifndef CONFIG_MODE #error CONFIG_MODE has to be defined diff --git a/tests/unit_tests/src/test/pixel_hfilter_test.c b/tests/unit_tests/src/test/pixel_hfilter_test.c index 08e02287..6c0a8450 100644 --- a/tests/unit_tests/src/test/pixel_hfilter_test.c +++ b/tests/unit_tests/src/test/pixel_hfilter_test.c @@ -272,15 +272,17 @@ TEST(pixel_hfilter, pixel_hfilter_update_scale__case1) memset(&state, 0, sizeof(state)); const float gain = 0.0f; + const size_t offset = 0; - pixel_hfilter_update_scale(&state, gain, 0); + pixel_hfilter_update_scale(&state, gain, offset); const unsigned exp_shift = 9; - const int32_t exp_acc_init = -128 * (1< Date: Wed, 21 Jun 2023 09:39:20 +0100 Subject: [PATCH 122/306] camera_api replace --- camera/api/camera_api.h | 12 ++++++------ camera/src/camera_api.c | 16 ++++++++-------- camera/src/packet_handler.c | 6 +++--- examples/take_picture_downsample/src/app.c | 4 ++-- examples/take_picture_raw/src/app_raw.c | 4 ++-- sensors/api/sensor.h | 20 ++++++++++---------- sensors/src/sensor_control.xc | 10 +++++----- tests/hardware_tests/test_timing/src/app.xc | 6 +++--- 8 files changed, 39 insertions(+), 39 deletions(-) diff --git a/camera/api/camera_api.h b/camera/api/camera_api.h index 1078621e..da8c3cfa 100644 --- a/camera/api/camera_api.h +++ b/camera/api/camera_api.h @@ -15,14 +15,14 @@ extern "C" { * * Initialize the camera API. Must be called before any other API functions. */ -void camera_api_init(); +void camera_init(); /** * CLIENT SIDE * * Stop the camera API. Must be called before exiting the program. */ -void camera_api_stop(); +void camera_stop(); /** * SERVER SIDE @@ -31,7 +31,7 @@ void camera_api_stop(); * * @return 1 if the client has requested to stop the camera, 0 otherwise */ -unsigned camera_api_check_stop(); +unsigned camera_check_stop(); /** * SERVER SIDE @@ -39,7 +39,7 @@ unsigned camera_api_check_stop(); * Called by the packet handler when a new row of raw image data is * available. Typically this will be Bayered image data. */ -void camera_api_new_row_raw( +void camera_new_row( const int8_t pixel_data[H_RAW], const unsigned row_index); @@ -49,7 +49,7 @@ void camera_api_new_row_raw( * Called by the packet handler when a new row of decimated image data is * available. */ -void camera_api_new_row_decimated( +void camera_new_row_decimated( const int8_t pixel_data[CH][W], const unsigned row_index); @@ -62,7 +62,7 @@ void camera_api_new_row_decimated( * * @return The row index of the captured row of pixels */ -unsigned camera_capture_row_raw( +unsigned camera_capture_row( int8_t pixel_data[W_RAW]); /** diff --git a/camera/src/camera_api.c b/camera/src/camera_api.c index 9e2c7d58..5a4c2ccc 100644 --- a/camera/src/camera_api.c +++ b/camera/src/camera_api.c @@ -18,14 +18,14 @@ streaming_channel_t c_user_api[3]; -void camera_api_init() +void camera_init() { c_user_api[CHAN_RAW] = s_chan_alloc(); c_user_api[CHAN_DEC] = s_chan_alloc(); c_user_api[CHAN_STOP] = s_chan_alloc(); } -unsigned camera_api_check_stop(){ +unsigned camera_check_stop(){ SELECT_RES( CASE_THEN(c_user_api[CHAN_STOP].end_b, user_handler), DEFAULT_THEN(default_handler)) @@ -37,11 +37,11 @@ unsigned camera_api_check_stop(){ } } -void camera_api_stop(){ +void camera_stop(){ s_chan_out_word(c_user_api[CHAN_STOP].end_a, (unsigned) 1); } -void camera_api_new_row_raw( +void camera_new_row( const int8_t pixel_data[W_RAW], const unsigned row_index) { @@ -61,7 +61,7 @@ void camera_api_new_row_raw( } } -void camera_api_new_row_decimated( +void camera_new_row_decimated( const int8_t pixel_data[CH][W], const unsigned row_index) { @@ -81,7 +81,7 @@ void camera_api_new_row_decimated( } } -unsigned camera_capture_row_raw( +unsigned camera_capture_row( int8_t pixel_data[W_RAW]) { s_chan_out_word(c_user_api[CHAN_RAW].end_b, (unsigned) &pixel_data[0]); @@ -106,12 +106,12 @@ unsigned camera_capture_image_raw( // Loop, capturing rows until we get one with row_index==0 do { - row_index = camera_capture_row_raw(&image_buff[0][0]); + row_index = camera_capture_row(&image_buff[0][0]); } while (row_index != 0); // Now capture the rest of the rows for (unsigned i=1; ipayload[0], ph_state.in_line_number); + camera_new_row((int8_t*) &pkt->payload[0], ph_state.in_line_number); // Bayer pattern is RGGB; even index rows have RG data, // odd index rows have GB data. @@ -153,7 +153,7 @@ void on_new_output_row( s_chan_out_word(c_out_row, (unsigned) &pix_out[0][0] ); // Service and user requests for decimated output - camera_api_new_row_decimated(pix_out, ph_state.out_line_number); + camera_new_row_decimated(pix_out, ph_state.out_line_number); ph_state.out_line_number++; } @@ -294,7 +294,7 @@ void mipi_packet_handler( mipi_packet_t * pkt = (mipi_packet_t*) s_chan_in_word(c_pkt); // Check is we are supose to stop or continue - unsigned stop = camera_api_check_stop(); + unsigned stop = camera_check_stop(); if (stop == 1){ // send stop to MipiReciever diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index 3e2a6eb6..ec76eb05 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -13,7 +13,7 @@ void user_app() { // Initialize camera api - camera_api_init(); + camera_init(); int8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; uint8_t temp_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; @@ -32,7 +32,7 @@ void user_app() printf("Image captured...\n"); // stop the threads and camera stream - camera_api_stop(); + camera_stop(); delay_milliseconds(100); // convert to uint8 with right dimentions diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index 61009d73..673f8714 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -12,7 +12,7 @@ void user_app_raw() { // Initialize camera api - camera_api_init(); + camera_init(); // set the input image to 0 int8_t image_buffer[H_RAW][W_RAW]; @@ -30,7 +30,7 @@ void user_app_raw() printf("Image captured...\n"); // stop the threads and camera stream - camera_api_stop(); + camera_stop(); delay_milliseconds(100); // Convert image from int8 to uint8 in-place diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index 2dd1aeed..450e701e 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -28,22 +28,22 @@ // Include custom libraries #if CONFIG_IMX219_SUPPORT #include "imx219.h" - #define camera_init(iic) imx219_init(iic) - #define camera_start(iic) imx219_stream_start(iic) - #define camera_stop(iic) imx219_stream_stop(iic) - #define camera_configure(iic) imx219_configure_mode(iic) - #define camera_set_exposure(iic,ex) imx219_set_gain_dB(iic,ex) + #define sensor_init(iic) imx219_init(iic) + #define sensor_start(iic) imx219_stream_start(iic) + #define sensor_stop(iic) imx219_stream_stop(iic) + #define sensor_configure(iic) imx219_configure_mode(iic) + #define sensor_set_exposure(iic,ex) imx219_set_gain_dB(iic,ex) #endif #if CONFIG_GC2145_SUPPORT #include "gc2145.h" /* //TODO - #define camera_init(iic) gcinit(iic) - #define camera_start(iic) gcstart(iic) - #define camera_stop(iic) gcstop(iic) - #define camera_configure(iic) gcconfigure(iic) - #define camera_set_exposure(iic,ex) gcsetexp(iic,ex) + #define sensor_init(iic) gcinit(iic) + #define sensor_start(iic) gcstart(iic) + #define sensor_stop(iic) gcstop(iic) + #define sensor_configure(iic) gcconfigure(iic) + #define sensor_set_exposure(iic,ex) gcsetexp(iic,ex) */ #endif diff --git a/sensors/src/sensor_control.xc b/sensors/src/sensor_control.xc index d73d17f1..40bdd5c6 100644 --- a/sensors/src/sensor_control.xc +++ b/sensors/src/sensor_control.xc @@ -10,11 +10,11 @@ void sensor_initialize(client interface i2c_master_if i2c){ int r = 0; - r |= camera_init(i2c); + r |= sensor_init(i2c); delay_milliseconds(100); - r |= camera_configure(i2c); + r |= sensor_configure(i2c); delay_milliseconds(600); - r |= camera_start(i2c); + r |= sensor_start(i2c); assert((r == 0)); // assert that camera is started and configured } @@ -27,11 +27,11 @@ void sensor_control( select { case sc.set_exposure(unsigned exposure): - camera_set_exposure(i2c, exposure); + sensor_set_exposure(i2c, exposure); break; case sc.stop(): - camera_stop(i2c); + sensor_stop(i2c); break; } } diff --git a/tests/hardware_tests/test_timing/src/app.xc b/tests/hardware_tests/test_timing/src/app.xc index c7e8c38c..9869a2c5 100644 --- a/tests/hardware_tests/test_timing/src/app.xc +++ b/tests/hardware_tests/test_timing/src/app.xc @@ -519,11 +519,11 @@ void mipi_main( // Start camera and its configurations int r = 0; - r |= camera_init(i2c); + r |= sensor_init(i2c); delay_milliseconds(100); //TODO include this inside the function - r |= camera_configure(i2c); + r |= sensor_configure(i2c); delay_milliseconds(500); - r |= camera_start(i2c); + r |= sensor_start(i2c); delay_milliseconds(2000); if (r != 0){ From b8934bd3b165509d7cca524394a928151ec8a34a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 21 Jun 2023 11:03:39 +0100 Subject: [PATCH 123/306] api rename issues, isp thread, isp settings --- camera/api/isp.h | 21 ++++-- camera/api/statistics.h | 47 ++++++------ camera/src/camera_main.xc | 4 +- camera/src/{isp.c => isp_functions.c} | 25 ------- camera/src/isp_pipeline.c | 69 +++++++++++++++++ camera/src/packet_handler.c | 22 +++--- camera/src/statistics.c | 103 +++----------------------- pytest.ini | 3 + sensors/_sony_imx219/imx219.xc | 4 +- sensors/api/sensor.h | 4 +- tests/hardware_tests/pytest.ini | 2 - tests/readme.rst | 9 +++ 12 files changed, 150 insertions(+), 163 deletions(-) rename camera/src/{isp.c => isp_functions.c} (89%) create mode 100644 camera/src/isp_pipeline.c create mode 100644 pytest.ini delete mode 100644 tests/hardware_tests/pytest.ini diff --git a/camera/api/isp.h b/camera/api/isp.h index 4ca13216..0d2a2560 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -5,11 +5,26 @@ #include // null #include -#include "statistics.h" +#include "statistics.h" // needed for global_stats_t // black level is sensor dependant (used by horizontal filter) #define BLACK_LEVEL 16 +// ISP settings +#define AE_MARGIN 0.1 // default marging for the auto exposure error +#define AE_INITIAL_EXPOSURE 35 // initial exposure value +#define AWB_gain_RED 1 +#define AWB_gain_GREEN 1 +#define AWB_gain_BLUE 1 +#define AWB_MAX 1.7 +#define AWB_MIN 0.8 + +// ---------------------------------- ISP PIPELINE ---------------------------------- +void isp_pipeline( + streaming_chanend_t c_img_in, + CLIENT_INTERFACE(sensor_control_if, sc_if) + ); + // ---------------------------------- AE/AGC ------------------------------ /** @@ -56,10 +71,6 @@ uint8_t AE_compute_new_exposure(float exposure, float skewness); // ---------------------------------- AWB ------------------------------ -// Initial channel scales -#define AWB_gain_RED 1 -#define AWB_gain_GREEN 1 -#define AWB_gain_BLUE 1 /** * struct to hold the calculated parameters for the ISP diff --git a/camera/api/statistics.h b/camera/api/statistics.h index 90aa6ddc..1e962515 100644 --- a/camera/api/statistics.h +++ b/camera/api/statistics.h @@ -17,23 +17,16 @@ extern "C" { #endif -// Size of the channel element code space -#define CHANNEL_CARDINALITY (1<> (APP_HISTOGRAM_QUANTIZATION_BITS)) +#define CH_CARD (1<> (HIST_QUANT_BITS)) // Number of histogram bins +#define APP_WB_PERCENTILE (0.94) + #if (HISTOGRAM_BIN_COUNT != 64) #error HISTOGRAM_BIN_COUNT value not currently supported. #endif -// The percentile to look for when applying white balance adjustments, as a -// fraction. (0.95 will find the value which 95% of pixels are less than or -// equal to) -#ifndef APP_WB_PERCENTILE -#define APP_WB_PERCENTILE (0.94) -#endif - -// Objects definitions +// ---------- Objects definitions ---------- typedef struct { int8_t pixels[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_WIDTH_PIXELS]; } low_res_image_row_t; @@ -53,34 +46,44 @@ typedef struct { typedef channel_stats_t global_stats_t[APP_IMAGE_CHANNEL_COUNT]; -// Statistics compute funtions + +// ---------- Api functions ---------- /** * Compute skewness of channel. * This is used by auto exposure * @param stats - * Pointer to channel statistics to update. */ -void compute_skewness(channel_stats_t *stats); +void stats_update_histogram(channel_histogram_t *hist, const int8_t pix[]); /** * Compute simple statistics for a set of data. * @param stats - * Pointer to the channel statistics to be computed */ -void compute_simple_stats(channel_stats_t *stats); +void stats_simple(channel_stats_t *stats); -void print_simple_stats(channel_stats_t *stats, unsigned channel); +/** + * @brief Compute the skewness of a channel + * + * @param stats + */ +void stats_skewness(channel_stats_t *stats); /** * Find the value for which (fraction) portion of pixels fall below that value. */ -void find_percentile(channel_stats_t *stats, const float fraction); +void stats_percentile(channel_stats_t *stats, const float fraction); + +/** + * @brief print the statistics of a channel + * + * @param stats + * @param channel + */ +void stats_print(channel_stats_t *stats, unsigned channel); + -// Thread function -void statistics_thread( - streaming_chanend_t c_img_in, - CLIENT_INTERFACE(sensor_control_if, sc_if) - ); #if defined(__XC__) || defined(__cplusplus) } diff --git a/camera/src/camera_main.xc b/camera/src/camera_main.xc index 0d45b9ea..c24cb479 100644 --- a/camera/src/camera_main.xc +++ b/camera/src/camera_main.xc @@ -12,7 +12,7 @@ #include "camera_main.h" #include "mipi_defines.h" #include "packet_handler.h" -#include "statistics.h" +#include "isp.h" #include "sensor_control.h" @@ -64,7 +64,7 @@ void camera_main( { MipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); mipi_packet_handler(c_pkt, c_ctrl, c_stat_thread); - statistics_thread(c_stat_thread, sc_if); + isp_pipeline(c_stat_thread, sc_if); sensor_control(sc_if, i2c); } } diff --git a/camera/src/isp.c b/camera/src/isp_functions.c similarity index 89% rename from camera/src/isp.c rename to camera/src/isp_functions.c index 63084896..e42f0e03 100644 --- a/camera/src/isp.c +++ b/camera/src/isp_functions.c @@ -1,8 +1,5 @@ #include "isp.h" -#define AE_MARGIN 0.1 // default marging for the auto exposure error -#define AE_INITIAL_EXPOSURE 35 // initial exposure value - // ---------------------------------- utils ------------------------------ static int8_t csign(float x) { @@ -94,10 +91,6 @@ uint8_t AE_compute_new_exposure(float exposure, float skewness) // ---------------------------------- AWB ------------------------------ -const float AWB_ceil = 255.0; -const float AWB_MAX = 1.7; -const float AWB_MIN = 0.8; - isp_params_t isp_params = { .channel_gain = { AWB_gain_RED, @@ -275,24 +268,6 @@ void AWB_print_gains(isp_params_t *isp_params){ // ---------------------------------- GAMMA ------------------------------ - -/* -const uint8_t gamma_1p8_s1[255] = { -0,12,17,22,25,29,32,35,37,40,42,44,47,49,51,53,55,57,58,60,62,64,65,67,69, -70,72,73,75,76,78,79,80,82,83,85,86,87,89,90,91,92,94,95,96,97,98,100,101, -102,103,104,105,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121, -122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,139, -140,141,142,143,144,145,146,146,147,148,149,150,151,152,152,153,154,155,156, -157,157,158,159,160,161,161,162,163,164,165,165,166,167,168,169,169,170,171, -172,172,173,174,175,175,176,177,178,178,179,180,181,181,182,183,183,184,185, -186,186,187,188,188,189,190,191,191,192,193,193,194,195,195,196,197,198,198, -199,200,200,201,202,202,203,204,204,205,206,206,207,208,208,209,209,210,211, -211,212,213,213,214,215,215,216,217,217,218,218,219,220,220,221,222,222,223, -223,224,225,225,226,226,227,228,228,229,230,230,231,231,232,233,233,234,234, -235,236,236,237,237,238,238,239,240,240,241,241,242,243,243,244,244,245,245, -246,247,247,248,248,249,249,250,251,251,252,252,253,253,254,255}; -*/ - const uint8_t gamma_1p8_s1[255] = { 0,12,17,22,25,29,32,35,37,40,42,44,47,49,51,53,55,57,58,60,62,64,65,67,69, 70,72,73,75,76,78,79,80,82,83,85,86,87,89,90,91,92,94,95,96,97,98,100,101, diff --git a/camera/src/isp_pipeline.c b/camera/src/isp_pipeline.c new file mode 100644 index 00000000..a83e327e --- /dev/null +++ b/camera/src/isp_pipeline.c @@ -0,0 +1,69 @@ +#include +#include "xccompat.h" + +#include "isp.h" +#include "statistics.h" + +/** + * Thread that computes the ISP pipeline for each pixel in the image. + * The statistics are stored in a struct which is used to perform ISP corrections. + * @param c_img_in - Channel end of the image. + */ +void isp_pipeline(streaming_chanend_t c_img_in, CLIENT_INTERFACE(sensor_control_if, sc_if)) +{ + // Outer loop iterates over frames + while (1) { + global_stats_t global_stats = {{0}}; + + // Inner loop iterates over rows within a frame + while (1) { + low_res_image_row_t* row = (low_res_image_row_t*) s_chan_in_word(c_img_in); + + if (row == NULL) { // Signal end of frame [1] + break; + } + + if (row == (low_res_image_row_t *) 1) { + // Stop the camera sensor + sensor_control_stop(sc_if); + // Exit + return; + } + + // Update histogram + for (uint8_t channel = 0; channel < APP_IMAGE_CHANNEL_COUNT; channel++) { + stats_update_histogram( + &global_stats[channel].histogram, + &row->pixels[channel][0]); + } + } + + // End of frame, compute statistics + for (uint8_t channel = 0; channel < APP_IMAGE_CHANNEL_COUNT; channel++) { + stats_simple(&global_stats[channel]); + stats_skewness(&global_stats[channel]); + stats_percentile(&global_stats[channel], APP_WB_PERCENTILE); + stats_print(&global_stats[channel], channel); + } + + // Adjust AE + uint8_t ae_done = AE_control_exposure(&global_stats, sc_if); + + // Adjust AWB + static unsigned run_once = 0; + if (ae_done == 1 && run_once == 0) { + AWB_compute_gains_white_max(&global_stats, &isp_params); // 0 + // AWB_compute_gains_white_patch(&global_stats, &isp_params); // 1 + // AWB_compute_gains_gray_world(&global_stats, &isp_params); // 2 + // AWB_compute_gains_percentile(&global_stats, &isp_params); // 3 + // AWB_compute_gains_static(&global_stats, &isp_params); // 4 + run_once = 1; // Set to 1 to run only once + } + + // Apply gamma curve + // TODO: Apply gamma curve here instead of in the user app + + // Print ISP info + AWB_print_gains(&isp_params); + } +} diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index b55ae7b9..4facfb89 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -147,10 +147,10 @@ unsigned handle_pixel_data( static void on_new_output_row( const int8_t pix_out[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_WIDTH_PIXELS], - streaming_chanend_t c_out_row) + streaming_chanend_t c_stats) { // Pass the output row along for statistics processing - s_chan_out_word(c_out_row, (unsigned) &pix_out[0][0] ); + s_chan_out_word(c_stats, (unsigned) &pix_out[0][0] ); // Service and user requests for decimated output camera_new_row_decimated(pix_out, ph_state.out_line_number); @@ -164,18 +164,18 @@ static void handle_frame_end( int8_t pix_out[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_WIDTH_PIXELS], const mipi_packet_t* pkt, - streaming_chanend_t c_out_row) + streaming_chanend_t c_stats) { // Drain the vertical filter's accumulators image_vfilter_drain(&pix_out[CHAN_RED][0], &vfilter_accs[CHAN_RED][0]); image_vfilter_drain(&pix_out[CHAN_GREEN][0], &vfilter_accs[CHAN_GREEN][0]); if(image_vfilter_drain(&pix_out[CHAN_BLUE][0], &vfilter_accs[CHAN_BLUE][0])){ // Pass final row(s) to the statistics thread - on_new_output_row(pix_out, c_out_row); + on_new_output_row(pix_out, c_stats); } // Signal statistics thread to do frame-end work by sending NULL. - s_chan_out_word(c_out_row, (unsigned) NULL); + s_chan_out_word(c_stats, (unsigned) NULL); } @@ -200,7 +200,7 @@ void handle_no_expected_lines() static void handle_packet( const mipi_packet_t* pkt, - streaming_chanend_t c_out_row) + streaming_chanend_t c_stats) { /* @@ -243,7 +243,7 @@ void handle_packet( break; case MIPI_DT_FRAME_END: - handle_frame_end(output_buff[out_dex], pkt, c_out_row); + handle_frame_end(output_buff[out_dex], pkt, c_stats); out_dex = 1 - out_dex; break; @@ -251,7 +251,7 @@ void handle_packet( handle_no_expected_lines(); if(handle_pixel_data(pkt, output_buff[out_dex])){ - on_new_output_row(output_buff[out_dex], c_out_row); + on_new_output_row(output_buff[out_dex], c_stats); out_dex = 1 - out_dex; } @@ -273,7 +273,7 @@ void handle_packet( void mipi_packet_handler( streaming_chanend_t c_pkt, streaming_chanend_t c_ctrl, - streaming_chanend_t c_out_row) + streaming_chanend_t c_stats) { /* * These buffers will be used to hold received MIPI packets while they're @@ -300,7 +300,7 @@ void mipi_packet_handler( // send stop to MipiReciever s_chan_out_word(c_pkt, (unsigned) NULL); // send stop to statistics - s_chan_out_word(c_out_row, (unsigned) 1); + s_chan_out_word(c_stats, (unsigned) 1); // end thread printf("\n\nMipiPacketHandler: stop\n\n"); return; @@ -314,7 +314,7 @@ void mipi_packet_handler( //const mipi_data_type_t data_type = MIPI_GET_DATA_TYPE(header); // unsigned time_start = measure_time(); - handle_packet(pkt, c_out_row); + handle_packet(pkt, c_stats); // unsigned time_proc = measure_time() - time_start; } diff --git a/camera/src/statistics.c b/camera/src/statistics.c index e5df1aa7..d0bfaa97 100644 --- a/camera/src/statistics.c +++ b/camera/src/statistics.c @@ -1,8 +1,5 @@ #include -#include -#include "xccompat.h" - #include "statistics.h" #include "isp.h" @@ -20,20 +17,20 @@ static const float histogram_norm_factor = (1.0 / (float) HISTOGRAM_TOTAL_SAMPL * @param hist - * pointer to the histogram to update. Must be large enough to accommodate the number of pixels in the image. * @param pix - array of pixel values to update the histogram with */ -void update_histogram( +void stats_update_histogram( channel_histogram_t* hist, const int8_t pix[]) { for(int k = 0; k < APP_IMAGE_WIDTH_PIXELS; k += APP_HISTOGRAM_SAMPLE_STEP){ int val = pix[k]; val += 128; // convert from int8_t to uint8_t - val >>= APP_HISTOGRAM_QUANTIZATION_BITS; + val >>= HIST_QUANT_BITS; hist->bins[val]++; } } -void compute_skewness(channel_stats_t *stats) +void stats_skewness(channel_stats_t *stats) { const float zk_values[] = { -1.000000, -0.907753, -0.821362, -0.740633, -0.665375, -0.595396, @@ -58,7 +55,7 @@ void compute_skewness(channel_stats_t *stats) } -void compute_simple_stats(channel_stats_t *stats) +void stats_simple(channel_stats_t *stats) { // Calculate the histogram uint8_t temp_min = 0; @@ -78,23 +75,13 @@ void compute_simple_stats(channel_stats_t *stats) } } // biased downwards due to truncation - stats->max = (temp_max << APP_HISTOGRAM_QUANTIZATION_BITS); - stats->min = (temp_min << APP_HISTOGRAM_QUANTIZATION_BITS); - stats->mean = (temp_mean) *(1 << APP_HISTOGRAM_QUANTIZATION_BITS) * histogram_norm_factor; + stats->max = (temp_max << HIST_QUANT_BITS); + stats->min = (temp_min << HIST_QUANT_BITS); + stats->mean = (temp_mean) *(1 << HIST_QUANT_BITS) * histogram_norm_factor; } -/* -typedef struct { - uint8_t min; - uint8_t max; - uint8_t percentile; - float skewness; - float mean; - channel_histogram_t histogram; -} channel_stats_t; -*/ -void print_simple_stats(channel_stats_t *stats, unsigned channel){ +void stats_print(channel_stats_t *stats, unsigned channel){ printf("ch:%d,Min:%d,Max:%d,Mean:%f,Skew:%f\n", channel, stats->min, @@ -103,7 +90,8 @@ void print_simple_stats(channel_stats_t *stats, unsigned channel){ stats->skewness); } -void find_percentile(channel_stats_t *stats, const float fraction) + +void stats_percentile(channel_stats_t *stats, const float fraction) { const unsigned threshold = fraction * HISTOGRAM_TOTAL_SAMPLES; // Could be optimized but fkeep it like this for timing reasons [2] @@ -113,81 +101,12 @@ void find_percentile(channel_stats_t *stats, const float fraction) for(int k = 0; k < HISTOGRAM_BIN_COUNT; k++){ unsigned new_total = total + stats->histogram.bins[k]; if(total < threshold && new_total >= threshold) - result = (k << APP_HISTOGRAM_QUANTIZATION_BITS); + result = (k << HIST_QUANT_BITS); total = new_total; } stats -> percentile = (uint8_t) result; } - -/** -* Thread that computes statistics for each pixel in the image. -* The statistics are stored in a struct which is used to perform -* isp corrections -* @param c_img_in - Channel end of the -*/ -void statistics_thread( - streaming_chanend_t c_img_in, - CLIENT_INTERFACE(sensor_control_if, sc_if)) -{ - // Outer loop iterates over frames - while(1){ - global_stats_t global_stats = {{0}}; - // Inner loop iterates over rows within a frame - while(1){ - - low_res_image_row_t* row = (low_res_image_row_t*) s_chan_in_word(c_img_in); - - if(row == NULL){ // Signal end of frame [1] - break; - } - - if(row == (low_res_image_row_t *) 1) - { - // stop the camera sensor - sensor_control_stop(sc_if); - // exit - return; - } - - // Update histogram - for(uint8_t channel = 0; channel < APP_IMAGE_CHANNEL_COUNT; channel++){ - update_histogram(&global_stats[channel].histogram, &row->pixels[channel][0]); - } - } - - // End of frame, compute statistics - for(uint8_t channel = 0; channel < APP_IMAGE_CHANNEL_COUNT; channel++){ - compute_skewness(&global_stats[channel]); - compute_simple_stats(&global_stats[channel]); - find_percentile(&global_stats[channel], APP_WB_PERCENTILE); - print_simple_stats(&global_stats[channel], channel); - } - - // Adjust AE - uint8_t ae_done = AE_control_exposure(&global_stats, sc_if); - - // Adjust AWB - static unsigned run_once = 0; - if (ae_done == 1 && run_once == 0){ - AWB_compute_gains_white_max(&global_stats, &isp_params); //0 - //AWB_compute_gains_white_patch(&global_stats, &isp_params); //1 - //AWB_compute_gains_gray_world(&global_stats, &isp_params); //2 - //AWB_compute_gains_percentile(&global_stats, &isp_params); //3 - //AWB_compute_gains_static(&global_stats, &isp_params); //4 - run_once = 1; // set to 1 to just run once - } - - // Apply gamma curve - //TODO here instead of user app - - // Print ISP info - AWB_print_gains(&isp_params); - } -} - - - // Notes /* [1] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..8c823a0a --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = -s +testpaths = "tests/hardware_tests" diff --git a/sensors/_sony_imx219/imx219.xc b/sensors/_sony_imx219/imx219.xc index 094b4365..1f560a21 100644 --- a/sensors/_sony_imx219/imx219.xc +++ b/sensors/_sony_imx219/imx219.xc @@ -16,8 +16,8 @@ static int i2c_write(client interface i2c_master_if i2c, int reg, int value) result = i2c.write_reg8_addr16(IMX219_I2C_ADDR, reg, value); if (result != I2C_REGOP_SUCCESS) { - // printf("Failed on address %02x value %02x\n", reg, value); - // TODO FIXME + printf("Failed on address %02x value %02x\n", reg, value); + assert(0); } return result != I2C_REGOP_SUCCESS ? -1 : 0; } diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index 450e701e..0254a645 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -156,8 +156,8 @@ // Number of bits to collapse channel cardinality (larger value results in fewer // histogram bins) -#ifndef APP_HISTOGRAM_QUANTIZATION_BITS -#define APP_HISTOGRAM_QUANTIZATION_BITS (2) +#ifndef HIST_QUANT_BITS +#define HIST_QUANT_BITS (2) #endif diff --git a/tests/hardware_tests/pytest.ini b/tests/hardware_tests/pytest.ini deleted file mode 100644 index e0113cd9..00000000 --- a/tests/hardware_tests/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -testpaths = "." diff --git a/tests/readme.rst b/tests/readme.rst index 55a489de..017da5fe 100644 --- a/tests/readme.rst +++ b/tests/readme.rst @@ -31,4 +31,13 @@ Run the following commands from the top level: Running the tests ============= + +Notes: * hardware tests require xscope_fileio installed +* run the following commands from fwk_camera top level. + +Run unit tests +- xsim --xscope "-offline trace.xmt" build/tests/unit_tests/test_camera.xe + +Run hardware tests +- pytest From 4ab2984a3e200e9804e8f329b87900aebc22b0bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 21 Jun 2023 11:57:48 +0100 Subject: [PATCH 124/306] adding minimum uknown packet handling --- camera/src/packet_handler.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index 4facfb89..a51f2c91 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -57,10 +57,13 @@ static void handle_unknown_packet( const mipi_packet_t* pkt) { - //TODO: manage uknown packets + const mipi_header_t header = pkt->header; + const mipi_data_type_t data_type = MIPI_GET_DATA_TYPE(header); // uknown packets could be the following: - // 1 - sensor specific packets (this could be useful for having more information about the frame) - // 2 - error packets (in this case mipi reciever will raise an exception, but in the future we want to handle them here) + // 1 - sensor specific packets (we let continue the app, uncomment print here for debug) + // printf("Unknown packet type: %d\n", data_type); + // 2 - invalid packets + assert(data_type < 0x3F && "Packet non valid"); } From 2fcd5f4deea57869352e1dd3e91997f4a5a6fe37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 21 Jun 2023 14:40:51 +0100 Subject: [PATCH 125/306] adding settings, exclude_patterns, substitutions --- doc/1_programming_guide/01_Introduction.rst | 6 +----- doc/exclude_patterns.inc | 2 ++ doc/substitutions.rst | 5 +++++ settings.json | 5 +++++ 4 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 doc/exclude_patterns.inc create mode 100644 doc/substitutions.rst create mode 100644 settings.json diff --git a/doc/1_programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst index 95532f2c..750e152d 100644 --- a/doc/1_programming_guide/01_Introduction.rst +++ b/doc/1_programming_guide/01_Introduction.rst @@ -1,6 +1,7 @@ Introduction ============= +.. include:: ../substitutions.rst .. contents:: Table of Contents Overview @@ -60,11 +61,6 @@ Software requirements: Additional Resources --------------------- -.. _MIPI: https://www.mipi.org/specifications/csi-2 -.. _XMOS: https://www.xmos.ai/ -.. _XMOSI2C: https://www.xmos.ai/download/lib_i2c-%5Buserguide%5D(5.0.0rc3).pdf -.. _XMOSProgrammingGuide: https://www.xmos.ai/download/XMOS-Programming-Guide-(documentation)(E).pdf -.. _IMX219: https://www.opensourceinstruments.com/Electronics/Data/IMX219PQ.pdf - MIPI CSI-2 specification: `MIPI`_ - XMOS I2C library user guide: `XMOSI2C`_ diff --git a/doc/exclude_patterns.inc b/doc/exclude_patterns.inc new file mode 100644 index 00000000..49de97d5 --- /dev/null +++ b/doc/exclude_patterns.inc @@ -0,0 +1,2 @@ +# The following patterns are to be excluded from the documentation build +tests diff --git a/doc/substitutions.rst b/doc/substitutions.rst new file mode 100644 index 00000000..234a8ea3 --- /dev/null +++ b/doc/substitutions.rst @@ -0,0 +1,5 @@ +.. _MIPI: https://www.mipi.org/specifications/csi-2 +.. _XMOS: https://www.xmos.ai/ +.. _XMOSI2C: https://www.xmos.ai/download/lib_i2c-%5Buserguide%5D(5.0.0rc3).pdf +.. _XMOSProgrammingGuide: https://www.xmos.ai/download/XMOS-Programming-Guide-(documentation)(E).pdf +.. _IMX219: https://www.opensourceinstruments.com/Electronics/Data/IMX219PQ.pdf diff --git a/settings.json b/settings.json new file mode 100644 index 00000000..a21c43c8 --- /dev/null +++ b/settings.json @@ -0,0 +1,5 @@ +{ + "title": "XCORE Camera Framework", + "project": "fwk_camera", + "version": "0.1" +} From 7a220f2e409f8972093b88282ac7624dc276bc3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 21 Jun 2023 15:11:03 +0100 Subject: [PATCH 126/306] resolving review items --- camera/api/statistics.h | 9 ++--- camera/src/camera_main.xc | 2 +- sensors/api/sensor.h | 4 +-- sensors/api/sensor_control.h | 2 +- sensors/src/sensor_control.xc | 6 ++-- tests/hardware_tests/test_timing/src/app.xc | 2 +- tests/readme.rst | 38 ++++++++++++++++++--- 7 files changed, 46 insertions(+), 17 deletions(-) diff --git a/camera/api/statistics.h b/camera/api/statistics.h index 1e962515..c2019df6 100644 --- a/camera/api/statistics.h +++ b/camera/api/statistics.h @@ -50,10 +50,11 @@ typedef channel_stats_t global_stats_t[APP_IMAGE_CHANNEL_COUNT]; // ---------- Api functions ---------- /** -* Compute skewness of channel. -* This is used by auto exposure -* @param stats - * Pointer to channel statistics to update. -*/ + * @brief Update the histogram given a row of pixels + * + * @param hist current histogram + * @param pix row of pixels + */ void stats_update_histogram(channel_histogram_t *hist, const int8_t pix[]); /** diff --git a/camera/src/camera_main.xc b/camera/src/camera_main.xc index c24cb479..3925debe 100644 --- a/camera/src/camera_main.xc +++ b/camera/src/camera_main.xc @@ -55,7 +55,7 @@ void camera_main( MIPI_CFG_CLK_DIV); // Initialize camera and its configurations - sensor_initialize(i2c); + sensor_startup(i2c); printf("\nCamera_started and configured...\n"); delay_milliseconds(1000); diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index 0254a645..85360f42 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -28,7 +28,7 @@ // Include custom libraries #if CONFIG_IMX219_SUPPORT #include "imx219.h" - #define sensor_init(iic) imx219_init(iic) + #define sensor_initialize(iic) imx219_init(iic) #define sensor_start(iic) imx219_stream_start(iic) #define sensor_stop(iic) imx219_stream_stop(iic) #define sensor_configure(iic) imx219_configure_mode(iic) @@ -39,7 +39,7 @@ #if CONFIG_GC2145_SUPPORT #include "gc2145.h" /* //TODO - #define sensor_init(iic) gcinit(iic) + #define sensor_initialize(iic) gcinit(iic) #define sensor_start(iic) gcstart(iic) #define sensor_stop(iic) gcstop(iic) #define sensor_configure(iic) gcconfigure(iic) diff --git a/sensors/api/sensor_control.h b/sensors/api/sensor_control.h index 0f1e017d..d9067c47 100644 --- a/sensors/api/sensor_control.h +++ b/sensors/api/sensor_control.h @@ -15,7 +15,7 @@ void sensor_control( server interface sensor_control_if sc, client interface i2c_master_if i2c); -void sensor_initialize(client interface i2c_master_if i2c); +void sensor_startup(client interface i2c_master_if i2c); #endif diff --git a/sensors/src/sensor_control.xc b/sensors/src/sensor_control.xc index 40bdd5c6..188687ab 100644 --- a/sensors/src/sensor_control.xc +++ b/sensors/src/sensor_control.xc @@ -4,13 +4,13 @@ #include #include "i2c.h" -#include "sensor_control.h" #include "sensor.h" +#include "sensor_control.h" -void sensor_initialize(client interface i2c_master_if i2c){ +void sensor_startup(client interface i2c_master_if i2c){ int r = 0; - r |= sensor_init(i2c); + r |= sensor_initialize(i2c); delay_milliseconds(100); r |= sensor_configure(i2c); delay_milliseconds(600); diff --git a/tests/hardware_tests/test_timing/src/app.xc b/tests/hardware_tests/test_timing/src/app.xc index 9869a2c5..c426ecba 100644 --- a/tests/hardware_tests/test_timing/src/app.xc +++ b/tests/hardware_tests/test_timing/src/app.xc @@ -519,7 +519,7 @@ void mipi_main( // Start camera and its configurations int r = 0; - r |= sensor_init(i2c); + r |= sensor_initialize(i2c); delay_milliseconds(100); //TODO include this inside the function r |= sensor_configure(i2c); delay_milliseconds(500); diff --git a/tests/readme.rst b/tests/readme.rst index 017da5fe..50855bc1 100644 --- a/tests/readme.rst +++ b/tests/readme.rst @@ -30,14 +30,42 @@ Run the following commands from the top level: Running the tests -============= +================= Notes: -* hardware tests require xscope_fileio installed -* run the following commands from fwk_camera top level. + 1. Hardware tests require `xscope_fileio` to be installed. + 2. Run the following commands from the `fwk_camera` top level. Run unit tests -- xsim --xscope "-offline trace.xmt" build/tests/unit_tests/test_camera.xe +-------------- + +.. code-block:: bash + + xsim --xscope "-offline trace.xmt" build/tests/unit_tests/test_camera.xe Run hardware tests -- pytest +------------------ + +.. code-block:: bash + + pytest +Running the tests +================= + +.. note:: + 1. Hardware tests require `xscope_fileio` to be installed. + 2. Run the following commands from the `fwk_camera` top level. + +Run unit tests +-------------- + +.. code-block:: bash + + xsim --xscope "-offline trace.xmt" build/tests/unit_tests/test_camera.xe + +Run hardware tests +------------------ + +.. code-block:: bash + + pytest From 35e56ffa23e2145f8a2734cb6492534ab6af591e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 21 Jun 2023 15:13:08 +0100 Subject: [PATCH 127/306] rst fix --- tests/readme.rst | 39 +++++++-------------------------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/tests/readme.rst b/tests/readme.rst index 50855bc1..32d48ebf 100644 --- a/tests/readme.rst +++ b/tests/readme.rst @@ -13,42 +13,17 @@ Build Tests Run the following commands from the top level: -.. tabs:: - .. tab:: Linux and Mac +.. code-block:: console + + cmake -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -B build + make -C build tests - .. code-block:: console - - cmake -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -B build - make -C build tests +.. code-block:: console - .. tab:: Windows + cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -B build + ninja -C build tests - .. code-block:: console - cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -B build - ninja -C build tests - - -Running the tests -================= - -Notes: - 1. Hardware tests require `xscope_fileio` to be installed. - 2. Run the following commands from the `fwk_camera` top level. - -Run unit tests --------------- - -.. code-block:: bash - - xsim --xscope "-offline trace.xmt" build/tests/unit_tests/test_camera.xe - -Run hardware tests ------------------- - -.. code-block:: bash - - pytest Running the tests ================= From ef088b8b8472a381cfcc6c40533b0ebd5c53bc8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 21 Jun 2023 15:36:02 +0100 Subject: [PATCH 128/306] review corrections 2 --- camera/src/camera_main.xc | 2 +- sensors/_sony_imx219/imx219.h | 6 +++--- sensors/_sony_imx219/imx219.xc | 8 ++++---- sensors/api/sensor.h | 14 +++++++------- sensors/api/sensor_control.h | 2 +- sensors/src/sensor_control.xc | 6 +++--- tests/hardware_tests/test_timing/src/app.xc | 2 +- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/camera/src/camera_main.xc b/camera/src/camera_main.xc index 3925debe..734a9e0f 100644 --- a/camera/src/camera_main.xc +++ b/camera/src/camera_main.xc @@ -55,7 +55,7 @@ void camera_main( MIPI_CFG_CLK_DIV); // Initialize camera and its configurations - sensor_startup(i2c); + sensor_start(i2c); printf("\nCamera_started and configured...\n"); delay_milliseconds(1000); diff --git a/sensors/_sony_imx219/imx219.h b/sensors/_sony_imx219/imx219.h index 317fffbb..7e784d9f 100644 --- a/sensors/_sony_imx219/imx219.h +++ b/sensors/_sony_imx219/imx219.h @@ -54,11 +54,11 @@ typedef struct // functions -int imx219_init(CLIENT_INTERFACE(i2c_master_if, i2c)); +int imx219_initialize(CLIENT_INTERFACE(i2c_master_if, i2c)); int imx219_stream_start(CLIENT_INTERFACE(i2c_master_if, i2c)); -int imx219_configure_mode(CLIENT_INTERFACE(i2c_master_if, i2c)); +int imx219_configure(CLIENT_INTERFACE(i2c_master_if, i2c)); int imx219_stream_stop(CLIENT_INTERFACE(i2c_master_if, i2c)); -int imx219_set_gain_dB(CLIENT_INTERFACE(i2c_master_if, i2c), uint32_t dBGain); +int imx219_set_exposure(CLIENT_INTERFACE(i2c_master_if, i2c), uint32_t dBGain); int imx219_set_binning(CLIENT_INTERFACE(i2c_master_if, i2c), uint32_t H_binning, uint32_t V_binning); int imx219_read(CLIENT_INTERFACE(i2c_master_if, i2c), uint16_t addr); void imx219_read_gains(CLIENT_INTERFACE(i2c_master_if, i2c), uint16_t values[5]); diff --git a/sensors/_sony_imx219/imx219.xc b/sensors/_sony_imx219/imx219.xc index 1f560a21..29b156d7 100644 --- a/sensors/_sony_imx219/imx219.xc +++ b/sensors/_sony_imx219/imx219.xc @@ -108,7 +108,7 @@ void imx219_read_gains(client interface i2c_master_if i2c, uint16_t values[5]){ } /// ------------------------------------------------------------------------------- -int imx219_init(client interface i2c_master_if i2c) +int imx219_initialize(client interface i2c_master_if i2c) { int ret = 0; // Send all registers that are common to all modes @@ -116,11 +116,11 @@ int imx219_init(client interface i2c_master_if i2c) // Configure two or four Lane mode ret = i2c_write_table_val(i2c, imx219_lanes_regs, sizeof(imx219_lanes_regs) / sizeof(imx219_lanes_regs[0])); // set gain - ret = imx219_set_gain_dB(i2c, GAIN_DB); + ret = imx219_set_exposure(i2c, GAIN_DB); return ret; } -int imx219_configure_mode(client interface i2c_master_if i2c) +int imx219_configure(client interface i2c_master_if i2c) { int ret = 0; // Apply default values of current mode @@ -144,7 +144,7 @@ int imx219_stream_stop(client interface i2c_master_if i2c){ return i2c_write_table(i2c, stop_regs, sizeof(stop_regs) / sizeof(stop_regs[0])); } -int imx219_set_gain_dB(client interface i2c_master_if i2c, +int imx219_set_exposure(client interface i2c_master_if i2c, uint32_t dBGain) { uint32_t time, again, dgain; diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index 85360f42..a9cd5e37 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -28,11 +28,11 @@ // Include custom libraries #if CONFIG_IMX219_SUPPORT #include "imx219.h" - #define sensor_initialize(iic) imx219_init(iic) - #define sensor_start(iic) imx219_stream_start(iic) - #define sensor_stop(iic) imx219_stream_stop(iic) - #define sensor_configure(iic) imx219_configure_mode(iic) - #define sensor_set_exposure(iic,ex) imx219_set_gain_dB(iic,ex) + #define sensor_initialize(iic) imx219_initialize(iic) + #define sensor_stream_start(iic) imx219_stream_start(iic) + #define sensor_stream_stop(iic) imx219_stream_stop(iic) + #define sensor_configure(iic) imx219_configure(iic) + #define sensor_set_exposure(iic,ex) imx219_set_exposure(iic,ex) #endif @@ -40,8 +40,8 @@ #include "gc2145.h" /* //TODO #define sensor_initialize(iic) gcinit(iic) - #define sensor_start(iic) gcstart(iic) - #define sensor_stop(iic) gcstop(iic) + #define sensor_stream_start(iic) gcstart(iic) + #define sensor_stream_stop(iic) gcstop(iic) #define sensor_configure(iic) gcconfigure(iic) #define sensor_set_exposure(iic,ex) gcsetexp(iic,ex) */ diff --git a/sensors/api/sensor_control.h b/sensors/api/sensor_control.h index d9067c47..7d7dcbe4 100644 --- a/sensors/api/sensor_control.h +++ b/sensors/api/sensor_control.h @@ -15,7 +15,7 @@ void sensor_control( server interface sensor_control_if sc, client interface i2c_master_if i2c); -void sensor_startup(client interface i2c_master_if i2c); +void sensor_start(client interface i2c_master_if i2c); #endif diff --git a/sensors/src/sensor_control.xc b/sensors/src/sensor_control.xc index 188687ab..bd23ac93 100644 --- a/sensors/src/sensor_control.xc +++ b/sensors/src/sensor_control.xc @@ -8,13 +8,13 @@ #include "sensor_control.h" -void sensor_startup(client interface i2c_master_if i2c){ +void sensor_start(client interface i2c_master_if i2c){ int r = 0; r |= sensor_initialize(i2c); delay_milliseconds(100); r |= sensor_configure(i2c); delay_milliseconds(600); - r |= sensor_start(i2c); + r |= sensor_stream_start(i2c); assert((r == 0)); // assert that camera is started and configured } @@ -31,7 +31,7 @@ void sensor_control( break; case sc.stop(): - sensor_stop(i2c); + sensor_stream_stop(i2c); break; } } diff --git a/tests/hardware_tests/test_timing/src/app.xc b/tests/hardware_tests/test_timing/src/app.xc index c426ecba..b7f0a9df 100644 --- a/tests/hardware_tests/test_timing/src/app.xc +++ b/tests/hardware_tests/test_timing/src/app.xc @@ -523,7 +523,7 @@ void mipi_main( delay_milliseconds(100); //TODO include this inside the function r |= sensor_configure(i2c); delay_milliseconds(500); - r |= sensor_start(i2c); + r |= sensor_stream_start(i2c); delay_milliseconds(2000); if (r != 0){ From 793665385ba8f641636db27c4080e2eb4abc6f50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 21 Jun 2023 15:45:49 +0100 Subject: [PATCH 129/306] due to bad copy paste, black level was gone --- sensors/api/sensor.h | 1 + 1 file changed, 1 insertion(+) diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index c365ecfb..664dfaae 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -33,6 +33,7 @@ #define sensor_stream_stop(iic) imx219_stream_stop(iic) #define sensor_configure(iic) imx219_configure(iic) #define sensor_set_exposure(iic,ex) imx219_set_exposure(iic,ex) + #define SENSOR_BLACK_LEVEL 16 #endif #if CONFIG_GC2145_SUPPORT From ac3d2c00a05a797c13ff6897cbe789ecc0a48a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Thu, 22 Jun 2023 09:45:34 +0100 Subject: [PATCH 130/306] first stable AWB version --- camera/api/isp.h | 19 +++- camera/api/statistics.h | 10 ++ camera/src/isp_functions.c | 105 ++++++++++++++------- camera/src/isp_pipeline.c | 12 ++- camera/src/statistics.c | 26 ++++- examples/take_picture_downsample/src/app.c | 5 +- sensors/api/sensor.h | 8 +- 7 files changed, 135 insertions(+), 50 deletions(-) diff --git a/camera/api/isp.h b/camera/api/isp.h index 0d2a2560..8605ab00 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -7,9 +7,6 @@ #include "statistics.h" // needed for global_stats_t -// black level is sensor dependant (used by horizontal filter) -#define BLACK_LEVEL 16 - // ISP settings #define AE_MARGIN 0.1 // default marging for the auto exposure error #define AE_INITIAL_EXPOSURE 35 // initial exposure value @@ -18,6 +15,7 @@ #define AWB_gain_BLUE 1 #define AWB_MAX 1.7 #define AWB_MIN 0.8 +#define APPLY_GAMMA 1 // ---------------------------------- ISP PIPELINE ---------------------------------- void isp_pipeline( @@ -114,7 +112,9 @@ void AWB_compute_gains_percentile(global_stats_t *gstats, isp_params_t *isp_para // ---------------------------------- GAMMA ------------------------------ // Gamma correction table -extern const uint8_t gamma_1p8_s1[255]; +extern const uint8_t gamma_1p8_s1[256]; +extern const uint8_t gamma_1p4_s1[256]; +extern const uint8_t gamma_new[256]; /** * @brief gamma correction function @@ -124,7 +124,16 @@ extern const uint8_t gamma_1p8_s1[255]; */ void isp_gamma_stride1(const uint32_t buffsize, uint8_t *img); -void isp_gamma_1p8(uint8_t *img_in, const size_t height, const size_t width, const size_t channels); +/** + * @brief compute gamma curve given an image and the gamma adjustement + * + * @param img_in pointer to the image + * @param gamma_curve pointer to the gamma curve + * @param height image height + * @param width image width + * @param channels channels in the image + */ +void isp_gamma(uint8_t *img_in, const uint8_t *gamma_curve, const size_t height, const size_t width, const size_t channels); // -------------------------- ROTATE/RESIZE ------------------------------------- diff --git a/camera/api/statistics.h b/camera/api/statistics.h index c2019df6..d51317a5 100644 --- a/camera/api/statistics.h +++ b/camera/api/statistics.h @@ -41,6 +41,9 @@ typedef struct { uint8_t percentile; float skewness; float mean; + uint32_t max_count; + uint32_t min_count; + uint32_t per_count; channel_histogram_t histogram; } channel_stats_t; @@ -75,6 +78,13 @@ void stats_skewness(channel_stats_t *stats); */ void stats_percentile(channel_stats_t *stats, const float fraction); +/** + * @brief Compute the volume of a channel after its percentile + * + * @param stats channel statistics + */ +void stats_percentile_volume(channel_stats_t *stats); + /** * @brief print the statistics of a channel * diff --git a/camera/src/isp_functions.c b/camera/src/isp_functions.c index e42f0e03..34afecf1 100644 --- a/camera/src/isp_functions.c +++ b/camera/src/isp_functions.c @@ -112,24 +112,23 @@ float AWB_clip_value(float tmp){ void AWB_compute_gains_percentile(global_stats_t *gstats, isp_params_t *isp_params){ // Adjust AWB - float tmp0=1.3; - float tmp1=0.8; - float tmp2=1.3; - - // percentile adjustement - printf("%d,%d,%d,%d,%d,%d,\n", - (*gstats)[0].min, - (*gstats)[0].max, - (*gstats)[1].min, - (*gstats)[1].max, - (*gstats)[2].min, - (*gstats)[2].max); + float tmp0=1; + float tmp1=1; + float tmp2=1; - tmp0 = 255.0 / (float)(*gstats)[0].percentile; // RED - tmp1 = 255.0 / (float)(*gstats)[1].percentile; // GREEN - tmp2 = 255.0 / (float)(*gstats)[2].percentile; // BLUE + uint8_t red_p = (*gstats)[0].percentile; + uint8_t green_p = (*gstats)[1].percentile; + uint8_t blue_p = (*gstats)[2].percentile; - + tmp0 = green_p/(float)red_p; + tmp1 = 1.0; + tmp2 = green_p/(float)blue_p; + + //tmp0 = 255.0 / (float)(*gstats)[0].percentile; // RED + // tmp1 = 255.0 / (float)(*gstats)[1].percentile; // GREEN + //tmp2 = 255.0 / (float)(*gstats)[2].percentile; // BLUE + + /* // add skewness contribution const float skmin = -1; const float skmax = 1; @@ -149,9 +148,19 @@ void AWB_compute_gains_percentile(global_stats_t *gstats, isp_params_t *isp_para tmp0 = 0.7*tmp0 + 0.3*gains[0]; tmp1 = 0.7*tmp1 + 0.3*gains[1]; tmp2 = 0.7*tmp2 + 0.3*gains[2]; + */ + uint32_t r_per_count = (*gstats)[0].per_count; + uint32_t g_per_count = (*gstats)[1].per_count; + uint32_t b_per_count = (*gstats)[2].per_count; + + float tmpA = (float)g_per_count/(float)r_per_count; + float tmpC = (float)g_per_count/(float)b_per_count; + + tmp0 = tmpA;//(tmp0 + tmpA)/2.0; + tmp2 = tmpC;//(tmp2 + tmpC)/2.0; tmp0 = AWB_clip_value(tmp0); - tmp1 = AWB_clip_value(tmp1); + //tmp1 = AWB_clip_value(tmp1); tmp2 = AWB_clip_value(tmp2); isp_params->channel_gain[0] = tmp0; @@ -192,7 +201,7 @@ void AWB_compute_gains_white_patch(global_stats_t *gstats, isp_params_t *isp_par void AWB_compute_gains_white_max(global_stats_t *gstats, isp_params_t *isp_params){ float alfa = 1.0; - const float beta = 1.0; + const float beta = 1.05; float gamma = 1.0; uint8_t Rmax = (*gstats)[0].max; // RED @@ -224,7 +233,7 @@ void AWB_compute_gains_white_max(global_stats_t *gstats, isp_params_t *isp_param float gamma2 = Gavg/Bavg; // armonic mean - float gww = 0.7; // grey world weight + float gww = 0.75; // grey world weight alfa = (1-gww)*alfa + gww*alfa2; gamma = (1-gww)*gamma + gww*gamma2; @@ -268,7 +277,7 @@ void AWB_print_gains(isp_params_t *isp_params){ // ---------------------------------- GAMMA ------------------------------ -const uint8_t gamma_1p8_s1[255] = { +const uint8_t gamma_1p8_s1[256] = { 0,12,17,22,25,29,32,35,37,40,42,44,47,49,51,53,55,57,58,60,62,64,65,67,69, 70,72,73,75,76,78,79,80,82,83,85,86,87,89,90,91,92,94,95,96,97,98,100,101, 102,103,104,105,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121, @@ -281,9 +290,44 @@ const uint8_t gamma_1p8_s1[255] = { 211,212,213,213,214,215,215,216,217,217,218,218,219,220,220,221,222,222,223, 223,224,225,225,226,226,227,228,228,229,230,230,231,231,232,233,233,234,234, 235,236,236,237,237,238,238,239,240,240,241,241,242,243,243,244,244,245,245, -246,247,247,247,246,245,244,243,242,241,240,239,238,237,236,235}; - - +246,247,247,247,246,245,244,243,242,241,240,239,238,237,236,235,236}; + + +const uint8_t gamma_1p4_s1[256] = { +0,0,0,1,3,5,8,10,12,13,15,17,19,20,22,24,25,27,28,30,31,33,34,36,37,39, +40,41,43,44,45,47,48,49,50,52,53,54,55,57,58,59,60,62,63,64,65,66,67,68,70,71, +72,73,74,75,76,77,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98, +99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,123, +124,125,126,127,128,129,130,131,132,133,133,134,135,136,137,138,139,140,141,141,142,143,144,145,146,147, +148,148,149,150,151,152,153,154,154,155,156,157,158,159,160,160,161,162,163,164,165,165,166,167,168,169, +170,170,171,172,173,174,174,175,176,177,178,178,179,180,181,182,182,183,184,185,186,186,187,188,189,190, +190,191,192,193,194,194,195,196,197,197,198,199,200,201,201,202,203,204,204,205,206,207,207,208,209,210, +210,211,212,213,213,214,215,216,216,217,218,219,219,220,221,222,222,223,224,225,225,226,227,228,228,229, +230,231,231,232,233,233,234,235,236,236,237,238,239,239,240,241,241,242,243,244,245,247}; + + +// gamma 1.8, with substract 10 and 1.05 multiplier +const uint8_t gamma_new[256] = { +0, 0, 0, 0, 0, 0, 0, 2, 5, 8, 11, 14, 16, +19, 21, 23, 26, 28, 30, 32, 34, 36, 38, 40, 42, 43, +45, 47, 49, 50, 52, 54, 55, 57, 59, 60, 62, 63, 65, +66, 68, 69, 71, 72, 73, 75, 76, 77, 79, 80, 82, 83, +84, 85, 87, 88, 89, 91, 92, 93, 94, 95, 97, 98, 99, +100, 101, 103, 104, 105, 106, 107, 108, 109, 110, 112, 113, 114, +115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, +128, 130, 131, 132, 133, 134, 134, 135, 136, 137, 138, 139, 140, +141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 152, +153, 154, 155, 156, 157, 158, 159, 160, 161, 161, 162, 163, 164, +165, 166, 167, 167, 168, 169, 170, 171, 172, 173, 173, 174, 175, +176, 177, 178, 178, 179, 180, 181, 182, 182, 183, 184, 185, 186, +186, 187, 188, 189, 190, 190, 191, 192, 193, 194, 194, 195, 196, +197, 197, 198, 199, 200, 200, 201, 202, 203, 203, 204, 205, 206, +206, 207, 208, 209, 209, 210, 211, 212, 212, 213, 214, 215, 215, +216, 217, 217, 218, 219, 220, 220, 221, 222, 222, 223, 224, 225, +225, 226, 227, 227, 228, 229, 229, 230, 231, 232, 232, 233, 234, +234, 235, 236, 236, 237, 238, 238, 239, 240, 240, 241, 242, 242, +243, 244, 244, 245, 246, 246, 247, 248, 248, 249, 250, 250, 251, +252, 252, 253, 254, 254, 255, 255, 255}; void isp_gamma_stride1(const uint32_t buffsize, uint8_t *img){ // gamma naming: 1p8_s1 = gamma 1.8 , with a stride of 1 @@ -293,21 +337,18 @@ void isp_gamma_stride1(const uint32_t buffsize, uint8_t *img){ } } -void isp_gamma_1p8( +void isp_gamma( uint8_t * img_in, + const uint8_t *gamma_curve, const size_t height, const size_t width, const size_t channels ) { - for(size_t k = 0; k < height; k++){ - for(size_t j = 0; j < width; j++){ - for(size_t c = 0; c < channels; c++){ - size_t index_in = c * (height * width) + k * width + j; - uint8_t value = img_in[index_in]; - img_in[index_in] = gamma_1p8_s1[value]; - } - } + ssize_t buffsize = height * width * channels; + for(ssize_t idx = 0; idx < buffsize; idx++){ + uint8_t val = img_in[idx]; + img_in[idx] = gamma_curve[val]; } } diff --git a/camera/src/isp_pipeline.c b/camera/src/isp_pipeline.c index a83e327e..7e55fd82 100644 --- a/camera/src/isp_pipeline.c +++ b/camera/src/isp_pipeline.c @@ -38,11 +38,13 @@ void isp_pipeline(streaming_chanend_t c_img_in, CLIENT_INTERFACE(sensor_control_ } } - // End of frame, compute statistics + // End of frame, compute statistics (order is important) for (uint8_t channel = 0; channel < APP_IMAGE_CHANNEL_COUNT; channel++) { stats_simple(&global_stats[channel]); stats_skewness(&global_stats[channel]); stats_percentile(&global_stats[channel], APP_WB_PERCENTILE); + stats_percentile_volume(&global_stats[channel]); + // print channel stats stats_print(&global_stats[channel], channel); } @@ -54,10 +56,10 @@ void isp_pipeline(streaming_chanend_t c_img_in, CLIENT_INTERFACE(sensor_control_ if (ae_done == 1 && run_once == 0) { AWB_compute_gains_white_max(&global_stats, &isp_params); // 0 // AWB_compute_gains_white_patch(&global_stats, &isp_params); // 1 - // AWB_compute_gains_gray_world(&global_stats, &isp_params); // 2 - // AWB_compute_gains_percentile(&global_stats, &isp_params); // 3 - // AWB_compute_gains_static(&global_stats, &isp_params); // 4 - run_once = 1; // Set to 1 to run only once + //AWB_compute_gains_gray_world(&global_stats, &isp_params); // 2 + //AWB_compute_gains_percentile(&global_stats, &isp_params); // 3 + //AWB_compute_gains_static(&global_stats, &isp_params); // 4 + run_once = 0; // Set to 1 to run only once } // Apply gamma curve diff --git a/camera/src/statistics.c b/camera/src/statistics.c index d0bfaa97..d59b0bbd 100644 --- a/camera/src/statistics.c +++ b/camera/src/statistics.c @@ -67,13 +67,17 @@ void stats_simple(channel_stats_t *stats) // mean temp_mean += bin_count * k; // max and min - if (bin_count != 0){ + if (bin_count != 0){ // the last that is not zero temp_max = k; - if (temp_min == 0){ + if (temp_min == 0){ // first time is zero, then min is set temp_min = k; } } } + // max and min count + stats->max_count = stats->histogram.bins[temp_max]; + stats->min_count = stats->histogram.bins[temp_min]; + // biased downwards due to truncation stats->max = (temp_max << HIST_QUANT_BITS); stats->min = (temp_min << HIST_QUANT_BITS); @@ -82,12 +86,16 @@ void stats_simple(channel_stats_t *stats) void stats_print(channel_stats_t *stats, unsigned channel){ - printf("ch:%d,Min:%d,Max:%d,Mean:%f,Skew:%f\n", + printf("ch:%d,Mi:%d,Ma:%d,Mean:%f,Sk:%f,pct:%d,mi_c:%lu,ma_c:%lu,pc:%lu\n", channel, stats->min, stats->max, stats->mean, - stats->skewness); + stats->skewness, + stats->percentile, + stats->min_count, + stats->max_count, + stats->per_count); } @@ -107,6 +115,16 @@ void stats_percentile(channel_stats_t *stats, const float fraction) stats -> percentile = (uint8_t) result; } +void stats_percentile_volume(channel_stats_t *stats) +{ + uint32_t bin_count = 0; + uint8_t percentile_point = stats -> percentile / 4; + for(int k = percentile_point; k < HISTOGRAM_BIN_COUNT; k++){ + bin_count += stats->histogram.bins[k]; + } + stats->per_count = bin_count; +} + // Notes /* [1] diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index ec76eb05..ef0dede4 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -7,8 +7,6 @@ #include "app.h" #include "isp.h" // needed for gamma -#define APPLY_GAMMA 1 - void user_app() { @@ -47,7 +45,8 @@ void user_app() // apply gamma correction #if APPLY_GAMMA - isp_gamma_1p8((uint8_t *) &temp_buffer[0][0][0], + isp_gamma((uint8_t *) &temp_buffer[0][0][0], + &gamma_new[0], APP_IMAGE_HEIGHT_PIXELS, APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_CHANNEL_COUNT); diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index 664dfaae..93a7c15b 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -33,7 +33,12 @@ #define sensor_stream_stop(iic) imx219_stream_stop(iic) #define sensor_configure(iic) imx219_configure(iic) #define sensor_set_exposure(iic,ex) imx219_set_exposure(iic,ex) - #define SENSOR_BLACK_LEVEL 16 + // #define SENSOR_BLACK_LEVEL 16 + #define SENSOR_BLACK_LEVEL 32 // we intentionally set it to 32 increase dynamic range with gamma + //TODO + // ideally we should have the sensor black_level here that does not change and then + // we can have a custom black level that we set in isp.h + // example: #define CUSTOM_BLACK_LEVEL SENSOR_BLACK_LEVEL + 20 #endif #if CONFIG_GC2145_SUPPORT @@ -44,6 +49,7 @@ #define sensor_stream_stop(iic) gcstop(iic) #define sensor_configure(iic) gcconfigure(iic) #define sensor_set_exposure(iic,ex) gcsetexp(iic,ex) + #define SENSOR_BLACK_LEVEL 0 */ #endif From 4fcb52c47ae0ec640c4d484d14a1514c6bdea24d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Thu, 22 Jun 2023 12:57:04 +0100 Subject: [PATCH 131/306] stable awb version --- camera/api/isp.h | 2 + camera/src/isp_functions.c | 110 +++++++++++++++++++++++++++++++++++-- camera/src/isp_pipeline.c | 14 ++--- 3 files changed, 112 insertions(+), 14 deletions(-) diff --git a/camera/api/isp.h b/camera/api/isp.h index 8605ab00..dc39f288 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -102,6 +102,8 @@ void AWB_compute_gains_white_max(global_stats_t *gstats, isp_params_t *isp_param */ void AWB_print_gains(isp_params_t *isp_params); +float AWB_compute_score(global_stats_t *gstats); + /** * @brief auto white balance control function based on percentile * diff --git a/camera/src/isp_functions.c b/camera/src/isp_functions.c index 34afecf1..963fbeef 100644 --- a/camera/src/isp_functions.c +++ b/camera/src/isp_functions.c @@ -26,8 +26,8 @@ uint8_t AE_control_exposure( if (printf_info){ printf("-----> adjustement done\n"); printf_info = 0; - return 1; } + return 1; } else{ // Adjust exposure new_exp = AE_compute_new_exposure((float)new_exp, sk); @@ -121,7 +121,7 @@ void AWB_compute_gains_percentile(global_stats_t *gstats, isp_params_t *isp_para uint8_t blue_p = (*gstats)[2].percentile; tmp0 = green_p/(float)red_p; - tmp1 = 1.0; + tmp1 = 1; tmp2 = green_p/(float)blue_p; //tmp0 = 255.0 / (float)(*gstats)[0].percentile; // RED @@ -156,11 +156,11 @@ void AWB_compute_gains_percentile(global_stats_t *gstats, isp_params_t *isp_para float tmpA = (float)g_per_count/(float)r_per_count; float tmpC = (float)g_per_count/(float)b_per_count; - tmp0 = tmpA;//(tmp0 + tmpA)/2.0; - tmp2 = tmpC;//(tmp2 + tmpC)/2.0; + tmp0 = (tmp0 + tmpA)/2.0; + tmp2 = (tmp2 + tmpC)/2.0; tmp0 = AWB_clip_value(tmp0); - //tmp1 = AWB_clip_value(tmp1); + tmp1 = AWB_clip_value(tmp1); tmp2 = AWB_clip_value(tmp2); isp_params->channel_gain[0] = tmp0; @@ -171,7 +171,7 @@ void AWB_compute_gains_percentile(global_stats_t *gstats, isp_params_t *isp_para void AWB_compute_gains_static(global_stats_t *gstats, isp_params_t *isp_params){ // Adjust AWB float tmp0=1.4; - float tmp1=1.0; + float tmp1=1.1; float tmp2=1.4; isp_params->channel_gain[0] = tmp0; @@ -274,7 +274,105 @@ void AWB_print_gains(isp_params_t *isp_params){ isp_params->channel_gain[2]); } +static +float var_fast(float a, float b, float c){ + a /= 255.0; + b /= 255.0; + c /= 255.0; + + float mean = (a + b + c) / 3.0; + float dA = a - mean; + float dB = b - mean; + float dC = c - mean; + return 33 * (dA * dA + dB * dB + dC * dC); // 100/3 +} + +float AWB_compute_score(global_stats_t *gstats){ + uint8_t valR, valG, valB; + // min + valR = (*gstats)[0].min; // RED + valG = (*gstats)[1].min; // GREEN + valB = (*gstats)[2].min; // BLUE + float sc1 = var_fast(valR, valG, valB); + // mean + valR = (*gstats)[0].mean; // RED + valG = (*gstats)[1].mean; // GREEN + valB = (*gstats)[2].mean; // BLUE + float sc2 = var_fast(valR, valG, valB); + // max + valR = (*gstats)[0].max; // RED + valG = (*gstats)[1].max; // GREEN + valB = (*gstats)[2].max; // BLUE + float sc3 = var_fast(valR, valG, valB); + // return score + float result = (sc1 + sc2 + sc3) / 3.0; + return result; +} + + +static +void AWB_best_choice(global_stats_t *global_stats, isp_params_t *isp_params){ + // awb variables + static float score = 0.0; + static float min_score = 100; + static int choice = -1; // because it will ++ at the beginning + static int min_choice = 0; + + static int rollover = 1; + + // compute score and set choice + score = AWB_compute_score(&global_stats); + if (score < min_score) { + min_score = score; + min_choice = choice; + } + + // dont change choice in 3 frames + static int wait_frames = 0; + if (wait_frames < 2) { + wait_frames++; + } + else{ + wait_frames = 0; + } + + // run over choices + if (wait_frames == 0) { + if (rollover == 1) { + choice = choice + 1; + } + else{ + choice = min_choice; + } + + if (choice == 4) { + rollover = 0; + } + } + + // print decisions + printf("--->>> sc:%f, ch:%d, msc:%f, mch:%d\n", score, choice, min_score, min_choice); + + // then choose the algorithm + switch (choice) + { + case 0: + AWB_compute_gains_static(&global_stats, &isp_params); + break; + case 1: + AWB_compute_gains_gray_world(&global_stats, &isp_params); + break; + case 2: + AWB_compute_gains_white_max(&global_stats, &isp_params); + break; + case 3: + AWB_compute_gains_white_patch(&global_stats, &isp_params); + break; + default: //do not apply any correction + break; + } +} // ---------------------------------- GAMMA ------------------------------ const uint8_t gamma_1p8_s1[256] = { diff --git a/camera/src/isp_pipeline.c b/camera/src/isp_pipeline.c index 7e55fd82..843052da 100644 --- a/camera/src/isp_pipeline.c +++ b/camera/src/isp_pipeline.c @@ -52,14 +52,11 @@ void isp_pipeline(streaming_chanend_t c_img_in, CLIENT_INTERFACE(sensor_control_ uint8_t ae_done = AE_control_exposure(&global_stats, sc_if); // Adjust AWB - static unsigned run_once = 0; - if (ae_done == 1 && run_once == 0) { - AWB_compute_gains_white_max(&global_stats, &isp_params); // 0 - // AWB_compute_gains_white_patch(&global_stats, &isp_params); // 1 - //AWB_compute_gains_gray_world(&global_stats, &isp_params); // 2 - //AWB_compute_gains_percentile(&global_stats, &isp_params); // 3 - //AWB_compute_gains_static(&global_stats, &isp_params); // 4 - run_once = 0; // Set to 1 to run only once + static unsigned int run_once = 0; + if (ae_done == 1 && run_once == 0) + { + AWB_compute_gains_white_max(&global_stats, &isp_params); + run_once = 1; // Set to 1 to run only once } // Apply gamma curve @@ -67,5 +64,6 @@ void isp_pipeline(streaming_chanend_t c_img_in, CLIENT_INTERFACE(sensor_control_ // Print ISP info AWB_print_gains(&isp_params); + } } From bb836cb4f2e069902a6c7a955ca43adec455d970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Thu, 22 Jun 2023 13:04:16 +0100 Subject: [PATCH 132/306] leaving clean awb --- camera/api/isp.h | 29 ++++-- camera/src/isp_functions.c | 103 +-------------------- examples/take_picture_downsample/src/app.c | 2 +- examples/take_picture_raw/src/app_raw.c | 2 +- 4 files changed, 27 insertions(+), 109 deletions(-) diff --git a/camera/api/isp.h b/camera/api/isp.h index dc39f288..e9e11e96 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -88,21 +88,29 @@ extern isp_params_t isp_params; */ void AWB_compute_gains_static(global_stats_t *gstats, isp_params_t *isp_params); -//TODO +/** + * @brief auto white balance control function based on white patch algorithm + * + * @param gstats structure containing the global statistics + * @param isp_params structure containing the current isp parameters + */ void AWB_compute_gains_white_patch(global_stats_t *gstats, isp_params_t *isp_params); +/** + * @brief auto white balance control function based on gray world algorithm + * + * @param gstats structure containing the global statistics + * @param isp_params structure containing the current isp parameters + */ void AWB_compute_gains_gray_world(global_stats_t *gstats, isp_params_t *isp_params); -void AWB_compute_gains_white_max(global_stats_t *gstats, isp_params_t *isp_params); - /** - * @brief aux function to print the auto white balancing gains + * @brief auto white balance control function based on white max algorithm * + * @param gstats structure containing the global statistics * @param isp_params structure containing the current isp parameters */ -void AWB_print_gains(isp_params_t *isp_params); - -float AWB_compute_score(global_stats_t *gstats); +void AWB_compute_gains_white_max(global_stats_t *gstats, isp_params_t *isp_params); /** * @brief auto white balance control function based on percentile @@ -112,6 +120,13 @@ float AWB_compute_score(global_stats_t *gstats); */ void AWB_compute_gains_percentile(global_stats_t *gstats, isp_params_t *isp_params); +/** + * @brief aux function to print the auto white balancing gains + * + * @param isp_params structure containing the current isp parameters + */ +void AWB_print_gains(isp_params_t *isp_params); + // ---------------------------------- GAMMA ------------------------------ // Gamma correction table extern const uint8_t gamma_1p8_s1[256]; diff --git a/camera/src/isp_functions.c b/camera/src/isp_functions.c index 963fbeef..f0b8573a 100644 --- a/camera/src/isp_functions.c +++ b/camera/src/isp_functions.c @@ -1,4 +1,5 @@ #include "isp.h" +#define INCLUDE_ABS 0 // ---------------------------------- utils ------------------------------ static @@ -6,11 +7,12 @@ int8_t csign(float x) { return (x > 0) - (x < 0); } +#if INCLUDE_ABS static float cabs(float x) { return x * csign(x); } - +#endif // ---------------------------------- AE / AGC ------------------------------ uint8_t AE_control_exposure( global_stats_t *global_stats, @@ -274,105 +276,6 @@ void AWB_print_gains(isp_params_t *isp_params){ isp_params->channel_gain[2]); } -static -float var_fast(float a, float b, float c){ - a /= 255.0; - b /= 255.0; - c /= 255.0; - - float mean = (a + b + c) / 3.0; - float dA = a - mean; - float dB = b - mean; - float dC = c - mean; - return 33 * (dA * dA + dB * dB + dC * dC); // 100/3 -} - -float AWB_compute_score(global_stats_t *gstats){ - uint8_t valR, valG, valB; - // min - valR = (*gstats)[0].min; // RED - valG = (*gstats)[1].min; // GREEN - valB = (*gstats)[2].min; // BLUE - float sc1 = var_fast(valR, valG, valB); - // mean - valR = (*gstats)[0].mean; // RED - valG = (*gstats)[1].mean; // GREEN - valB = (*gstats)[2].mean; // BLUE - float sc2 = var_fast(valR, valG, valB); - // max - valR = (*gstats)[0].max; // RED - valG = (*gstats)[1].max; // GREEN - valB = (*gstats)[2].max; // BLUE - float sc3 = var_fast(valR, valG, valB); - // return score - float result = (sc1 + sc2 + sc3) / 3.0; - return result; -} - - -static -void AWB_best_choice(global_stats_t *global_stats, isp_params_t *isp_params){ - // awb variables - static float score = 0.0; - static float min_score = 100; - - static int choice = -1; // because it will ++ at the beginning - static int min_choice = 0; - - static int rollover = 1; - - // compute score and set choice - score = AWB_compute_score(&global_stats); - if (score < min_score) { - min_score = score; - min_choice = choice; - } - - // dont change choice in 3 frames - static int wait_frames = 0; - if (wait_frames < 2) { - wait_frames++; - } - else{ - wait_frames = 0; - } - - // run over choices - if (wait_frames == 0) { - if (rollover == 1) { - choice = choice + 1; - } - else{ - choice = min_choice; - } - - if (choice == 4) { - rollover = 0; - } - } - - // print decisions - printf("--->>> sc:%f, ch:%d, msc:%f, mch:%d\n", score, choice, min_score, min_choice); - - // then choose the algorithm - switch (choice) - { - case 0: - AWB_compute_gains_static(&global_stats, &isp_params); - break; - case 1: - AWB_compute_gains_gray_world(&global_stats, &isp_params); - break; - case 2: - AWB_compute_gains_white_max(&global_stats, &isp_params); - break; - case 3: - AWB_compute_gains_white_patch(&global_stats, &isp_params); - break; - default: //do not apply any correction - break; - } -} // ---------------------------------- GAMMA ------------------------------ const uint8_t gamma_1p8_s1[256] = { diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index ef0dede4..d0715ea4 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -20,7 +20,7 @@ void user_app() memset(image_buffer, -128, sizeof(image_buffer)); // Wait for the image to set exposure - delay_milliseconds(5000); + delay_milliseconds(4000); printf("Requesting image...\n"); // grab a frame if(camera_capture_image(image_buffer)){ diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index 673f8714..f0f2273a 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -19,7 +19,7 @@ void user_app_raw() memset(image_buffer, -128, H_RAW * W_RAW); // wait for the camera to set I2C parameters - delay_milliseconds(5000); + delay_milliseconds(4000); // Request an image printf("Requesting image...\n"); From d454dbaee219c4b2972aaa9c5ce03f2a485cafff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Thu, 22 Jun 2023 13:18:43 +0100 Subject: [PATCH 133/306] white max cleanup --- camera/src/isp_functions.c | 51 ++++++++++++++------------------------ 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/camera/src/isp_functions.c b/camera/src/isp_functions.c index f0b8573a..4144d51c 100644 --- a/camera/src/isp_functions.c +++ b/camera/src/isp_functions.c @@ -201,43 +201,30 @@ void AWB_compute_gains_white_patch(global_stats_t *gstats, isp_params_t *isp_par } -void AWB_compute_gains_white_max(global_stats_t *gstats, isp_params_t *isp_params){ - float alfa = 1.0; - const float beta = 1.05; - float gamma = 1.0; - - uint8_t Rmax = (*gstats)[0].max; // RED - uint8_t Gmax = (*gstats)[1].max; // GREEN - uint8_t Bmax = (*gstats)[2].max; // BLUE - - // correct to have similar white max - uint32_t count_max_red = (*gstats)[0].histogram.bins[Rmax]; - uint32_t count_max_green = (*gstats)[1].histogram.bins[Gmax]; - uint32_t count_max_blue = (*gstats)[2].histogram.bins[Bmax]; - - // false values avoidance - if (count_max_red != 0 && count_max_green != 0 && count_max_blue != 0){ - - if (count_max_green > count_max_red){ - alfa = (count_max_green/(float)count_max_red); - } +void AWB_compute_gains_white_max(global_stats_t *gstats, isp_params_t *isp_params){ + // we assume green constant + const float beta = 1.0; - if (count_max_green > count_max_blue){ - gamma = (count_max_green/(float)count_max_blue); - } - } - - // then compute the mean with grey world + // 1 - Grey world float Ravg = (*gstats)[0].mean; // RED float Gavg = (*gstats)[1].mean; // GREEN float Bavg = (*gstats)[2].mean; // BLUE - float alfa2 = Gavg/Ravg; - float gamma2 = Gavg/Bavg; - // armonic mean - float gww = 0.75; // grey world weight - alfa = (1-gww)*alfa + gww*alfa2; - gamma = (1-gww)*gamma + gww*gamma2; + float alfa = Gavg/Ravg; + float gamma = Gavg/Bavg; + + // 2 - Percentile volumne + uint32_t r_per_count = (*gstats)[0].per_count; + uint32_t g_per_count = (*gstats)[1].per_count; + uint32_t b_per_count = (*gstats)[2].per_count; + + float alfa2 = (float)g_per_count/(float)r_per_count; + float gamma2 = (float)g_per_count/(float)b_per_count; + + // Weighted mean + float gww = 0.7; // grey world weight + alfa = gww*alfa + (1-gww)*alfa2; + gamma = gww*gamma + (1-gww)*gamma2; // clip the values alfa = AWB_clip_value(alfa); From 51990367d0a2f0115e5cfe014fd906851325d77a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Thu, 22 Jun 2023 16:27:35 +0100 Subject: [PATCH 134/306] New doc structure --- doc/1_programming_guide/01_Introduction.rst | 18 +++++++++--------- .../02_Architecture_and_Design.rst | 2 +- .../03_Building_the_Software.rst | 4 +--- .../04_Software_test_and_development.rst | 4 ++++ doc/1_programming_guide/05_Examples.rst | 2 ++ doc/1_programming_guide/06_Troubleshooting.rst | 2 ++ doc/1_programming_guide/index.rst | 14 +++++++------- doc/2_quick_start_guide/.gitkeep | 0 doc/2_quick_start_guide/01_Introduction.rst | 4 ++++ doc/2_quick_start_guide/index.rst | 8 ++++++++ doc/Doxyfile.inc | 10 ++++++++++ doc/index.rst | 11 +++++++++++ doc/substitutions.rst | 2 ++ settings.json | 2 +- 14 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 doc/1_programming_guide/04_Software_test_and_development.rst create mode 100644 doc/1_programming_guide/05_Examples.rst create mode 100644 doc/1_programming_guide/06_Troubleshooting.rst create mode 100644 doc/2_quick_start_guide/.gitkeep create mode 100644 doc/2_quick_start_guide/01_Introduction.rst create mode 100644 doc/2_quick_start_guide/index.rst create mode 100644 doc/Doxyfile.inc create mode 100644 doc/index.rst diff --git a/doc/1_programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst index 750e152d..3a28b8c7 100644 --- a/doc/1_programming_guide/01_Introduction.rst +++ b/doc/1_programming_guide/01_Introduction.rst @@ -2,7 +2,6 @@ Introduction ============= .. include:: ../substitutions.rst -.. contents:: Table of Contents Overview --------- @@ -10,15 +9,16 @@ The purpose of this programming guide is to provide developers with a comprehens The architecture consists of several key components that work together to facilitate camera communication and data processing. These components include: -- Camera hardware and camera interface -- Camera drivers -- User application / user interface -- Image signal processing -- I/O +1. Camera hardware and camera interface +2. Camera drivers +3. User application +4. Image signal processing +5. I/O (Using Xscope_fileIO) + +Here below a high level block diagram of the FWK_Camera architecture: .. figure:: images/1_high_level_view.png :alt: Alternate text for the image - :width: 400px :align: center High level block diagram of fwk camera. @@ -55,8 +55,8 @@ Hardware requirements: Software requirements: -- XMOS tools: https://www.xmos.ai/software-tools/ -- FWK_Camera repository +- XMOS tools: `SW_TOOLS`_ +- FWK_Camera repository: `GH_FWK_CAMERA`_ - CMake, Ninja (Windows) Additional Resources diff --git a/doc/1_programming_guide/02_Architecture_and_Design.rst b/doc/1_programming_guide/02_Architecture_and_Design.rst index 6ac640a1..9d1f8e4d 100644 --- a/doc/1_programming_guide/02_Architecture_and_Design.rst +++ b/doc/1_programming_guide/02_Architecture_and_Design.rst @@ -1,7 +1,7 @@ Architecture and Design ======================= -.. contents:: Table of Contents +.. include:: ../substitutions.rst Introduction ------------- diff --git a/doc/1_programming_guide/03_Building_the_Software.rst b/doc/1_programming_guide/03_Building_the_Software.rst index 53e2264a..c0c195df 100644 --- a/doc/1_programming_guide/03_Building_the_Software.rst +++ b/doc/1_programming_guide/03_Building_the_Software.rst @@ -3,8 +3,7 @@ Building the Software This section will provide details on how the software is constructed. The basic steps and build requirements can be found in the README.md file which is distributed with the source. -.. contents:: Table of Contents - +.. include:: ../substitutions.rst Requirements ------------------------------------------ @@ -20,4 +19,3 @@ Adding new files Building the host app (xscope_fileio) ------------------------------------------ - diff --git a/doc/1_programming_guide/04_Software_test_and_development.rst b/doc/1_programming_guide/04_Software_test_and_development.rst new file mode 100644 index 00000000..c21656ff --- /dev/null +++ b/doc/1_programming_guide/04_Software_test_and_development.rst @@ -0,0 +1,4 @@ +Introduction +============= + +//TODO include how to ad da new camera diff --git a/doc/1_programming_guide/05_Examples.rst b/doc/1_programming_guide/05_Examples.rst new file mode 100644 index 00000000..250cc8bf --- /dev/null +++ b/doc/1_programming_guide/05_Examples.rst @@ -0,0 +1,2 @@ +Introduction +============= diff --git a/doc/1_programming_guide/06_Troubleshooting.rst b/doc/1_programming_guide/06_Troubleshooting.rst new file mode 100644 index 00000000..250cc8bf --- /dev/null +++ b/doc/1_programming_guide/06_Troubleshooting.rst @@ -0,0 +1,2 @@ +Introduction +============= diff --git a/doc/1_programming_guide/index.rst b/doc/1_programming_guide/index.rst index 4431e39d..8d741e2e 100644 --- a/doc/1_programming_guide/index.rst +++ b/doc/1_programming_guide/index.rst @@ -1,13 +1,13 @@ -Fwk camera Programming Guide Documentation +******************* +Programming Guide +******************* .. toctree:: - :maxdepth: 2 + :maxdepth: 1 01_Introduction 02_Architecture_and_Design 03_Building_the_Software - 04_Testing_the_Software - 05_Modifying_the_Software - 06_Adding_New_Cameras - 07_Examples - 08_Troubleshooting + 04_Software_test_and_development + 05_Examples + 06_Troubleshooting diff --git a/doc/2_quick_start_guide/.gitkeep b/doc/2_quick_start_guide/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/doc/2_quick_start_guide/01_Introduction.rst b/doc/2_quick_start_guide/01_Introduction.rst new file mode 100644 index 00000000..600cfb0d --- /dev/null +++ b/doc/2_quick_start_guide/01_Introduction.rst @@ -0,0 +1,4 @@ +Intro quick +============= + +loremp ipsum diff --git a/doc/2_quick_start_guide/index.rst b/doc/2_quick_start_guide/index.rst new file mode 100644 index 00000000..5c93d536 --- /dev/null +++ b/doc/2_quick_start_guide/index.rst @@ -0,0 +1,8 @@ +******************* +Quick start guide +******************* + +.. toctree:: + :maxdepth: 1 + + 01_Introduction diff --git a/doc/Doxyfile.inc b/doc/Doxyfile.inc new file mode 100644 index 00000000..065539e2 --- /dev/null +++ b/doc/Doxyfile.inc @@ -0,0 +1,10 @@ +# This file provides overrides to the Doxyfile configuration +PROJECT_NAME = FWK_CAMERA +PROJECT_BRIEF = "Camera interface for Xcore AI" +PREDEFINED = __DOXYGEN__=1 +PREDEFINED += DWORD_ALIGNED= +PREDEFINED += __attribute__((weak))= +PREDEFINED += C_API= MA_C_API= C_API_START= C_API_END= EXTERN_C= +INPUT += camera/ +ALIASES += "beginrst=^^\verbatim embed:rst^^" +ALIASES += "endrst=\endverbatim" diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 00000000..b0139911 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,11 @@ +################ +Framework Camera +################ + +Framework of camera processing libraries for XCORE.AI. + +.. toctree:: + :maxdepth: 1 + + ./1_programming_guide/index + ./2_quick_start_guide/index diff --git a/doc/substitutions.rst b/doc/substitutions.rst index 234a8ea3..8f8b99c0 100644 --- a/doc/substitutions.rst +++ b/doc/substitutions.rst @@ -3,3 +3,5 @@ .. _XMOSI2C: https://www.xmos.ai/download/lib_i2c-%5Buserguide%5D(5.0.0rc3).pdf .. _XMOSProgrammingGuide: https://www.xmos.ai/download/XMOS-Programming-Guide-(documentation)(E).pdf .. _IMX219: https://www.opensourceinstruments.com/Electronics/Data/IMX219PQ.pdf +.. _GH_FWK_CAMERA: https://github.com/xmos/fwk_camera +.. _SW_TOOLS: https://www.xmos.ai/software-tools/ diff --git a/settings.json b/settings.json index a21c43c8..a622df9b 100644 --- a/settings.json +++ b/settings.json @@ -1,5 +1,5 @@ { - "title": "XCORE Camera Framework", + "title": "XMOS FWK Camera", "project": "fwk_camera", "version": "0.1" } From 4c0838cfeafa55a12312721773fcca78d2e6d8a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Fri, 23 Jun 2023 09:21:00 +0100 Subject: [PATCH 135/306] adding release notes --- doc/3_release_notes/index.rst | 8 +++++ doc/3_release_notes/release_notes.rst | 47 +++++++++++++++++++++++++++ doc/index.rst | 1 + 3 files changed, 56 insertions(+) create mode 100644 doc/3_release_notes/index.rst create mode 100644 doc/3_release_notes/release_notes.rst diff --git a/doc/3_release_notes/index.rst b/doc/3_release_notes/index.rst new file mode 100644 index 00000000..5bf65b14 --- /dev/null +++ b/doc/3_release_notes/index.rst @@ -0,0 +1,8 @@ +******************* +Release Notes +******************* + +.. toctree:: + :maxdepth: 1 + + release_notes diff --git a/doc/3_release_notes/release_notes.rst b/doc/3_release_notes/release_notes.rst new file mode 100644 index 00000000..f1f638ca --- /dev/null +++ b/doc/3_release_notes/release_notes.rst @@ -0,0 +1,47 @@ +Release Notes +============= + +Version 0.0.1 (YYYY-MM-DD) +--------------------------- + +New Features: +************* + +- [Feature 1]: [Description] +- [Feature 2]: [Description] +- ... + +Enhancements: +************* + +- [Enhancement 1]: [Description] +- [Enhancement 2]: [Description] +- ... + +Bug Fixes: +****************** + +- [Bug Fix 1]: [Description] +- [Bug Fix 2]: [Description] +- ... + +Changes: +****************** + +- [Change 1]: [Description] +- [Change 2]: [Description] +- ... + +Deprecations and Removals: +****************************** + +- [Deprecation 1]: [Description] +- [Deprecation 2]: [Description] +- ... + +Known Issues: +************* + +- [Issue 1]: [Description] +- [Issue 2]: [Description] +- ... diff --git a/doc/index.rst b/doc/index.rst index b0139911..738952c6 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -9,3 +9,4 @@ Framework of camera processing libraries for XCORE.AI. ./1_programming_guide/index ./2_quick_start_guide/index + ./3_release_notes/index From ac0a6fad49949ee4abbb143ba98c912d0021d45c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Fri, 23 Jun 2023 11:31:22 +0100 Subject: [PATCH 136/306] adding gamma time test --- camera/api/camera_utils.h | 6 +- camera/src/isp_functions.c | 13 +++- camera/src/isp_pipeline.c | 2 +- tests/readme.rst | 4 +- tests/unit_tests/src/main.c | 6 +- .../{isp_test.c => color_conversion_test.c} | 18 +++-- tests/unit_tests/src/test/gamma_timing_test.c | 75 +++++++++++++++++++ 7 files changed, 107 insertions(+), 17 deletions(-) rename tests/unit_tests/src/test/{isp_test.c => color_conversion_test.c} (87%) create mode 100644 tests/unit_tests/src/test/gamma_timing_test.c diff --git a/camera/api/camera_utils.h b/camera/api/camera_utils.h index 6542a2b1..8c6ecf2d 100644 --- a/camera/api/camera_utils.h +++ b/camera/api/camera_utils.h @@ -5,12 +5,14 @@ #include "sensor.h" -#define PRINT_TIME(a,b) printf("%d\n", b - a); - #if defined(__XC__) || defined(__cplusplus) extern "C" { #endif +// Print macros +#define PRINT_TIME(a,b) printf("%d\n", b - a); +#define PRINT_NAME_TIME(name,time) \ + printf("\t%s timing: %u ticks, %.2fms\n", name, time, (float)time * 0.00001); /** * Measure the cpu ticks diff --git a/camera/src/isp_functions.c b/camera/src/isp_functions.c index 4144d51c..78732704 100644 --- a/camera/src/isp_functions.c +++ b/camera/src/isp_functions.c @@ -21,6 +21,7 @@ uint8_t AE_control_exposure( // Initial exposure static uint8_t new_exp = AE_INITIAL_EXPOSURE; static uint8_t printf_info = 1; + static uint8_t give_up = 0; // Compute skewness and adjust exposure if needed float sk = AE_compute_mean_skewness(global_stats); @@ -35,6 +36,14 @@ uint8_t AE_control_exposure( new_exp = AE_compute_new_exposure((float)new_exp, sk); sensor_control_set_exposure(sc_if, (uint8_t)new_exp); printf_info = 1; + // if it is too dark, give up but continue with awb + if (new_exp > 70){ + give_up++; + if (give_up > 5){ + give_up = 0; + return 1; + } + } } return 0; } @@ -60,9 +69,9 @@ inline uint8_t AE_is_adjusted(float sk) { uint8_t AE_compute_new_exposure(float exposure, float skewness) { - static float a = 0; // minimum value for exposure + static float a = 0; // minimum value for exposure static float fa = -1; // minimimum skewness - static float b = 80; // maximum value for exposure + static float b = 84; // maximum value for exposure static float fb = 1; // minimum skewness static int count = 0; float c = exposure; diff --git a/camera/src/isp_pipeline.c b/camera/src/isp_pipeline.c index 843052da..56d9ace3 100644 --- a/camera/src/isp_pipeline.c +++ b/camera/src/isp_pipeline.c @@ -56,7 +56,7 @@ void isp_pipeline(streaming_chanend_t c_img_in, CLIENT_INTERFACE(sensor_control_ if (ae_done == 1 && run_once == 0) { AWB_compute_gains_white_max(&global_stats, &isp_params); - run_once = 1; // Set to 1 to run only once + run_once = 0; // Set to 1 to run only once } // Apply gamma curve diff --git a/tests/readme.rst b/tests/readme.rst index 32d48ebf..d37f1eb4 100644 --- a/tests/readme.rst +++ b/tests/readme.rst @@ -36,7 +36,9 @@ Run unit tests .. code-block:: bash - xsim --xscope "-offline trace.xmt" build/tests/unit_tests/test_camera.xe + xsim --xscope "-offline trace.xmt" build/tests/unit_tests/test_camera.xe + or + xrun --xscope build/tests/unit_tests/test_camera.xe Run hardware tests ------------------ diff --git a/tests/unit_tests/src/main.c b/tests/unit_tests/src/main.c index 80139168..bcbaa8cd 100644 --- a/tests/unit_tests/src/main.c +++ b/tests/unit_tests/src/main.c @@ -1,7 +1,6 @@ // Copyright 2020-2022 XMOS LIMITED. // This Software is subject to the terms of the XMOS Public Licence: Version 1. - #include #include #include "unity_fixture.h" @@ -19,7 +18,8 @@ int main( RUN_TEST_GROUP(pixel_hfilter); RUN_TEST_GROUP(pixel_vfilter); - RUN_TEST_GROUP(isp_tests); + RUN_TEST_GROUP(color_conversion); + RUN_TEST_GROUP(gamma); return UNITY_END(); -} \ No newline at end of file +} diff --git a/tests/unit_tests/src/test/isp_test.c b/tests/unit_tests/src/test/color_conversion_test.c similarity index 87% rename from tests/unit_tests/src/test/isp_test.c rename to tests/unit_tests/src/test/color_conversion_test.c index 849daff1..10025ecb 100644 --- a/tests/unit_tests/src/test/isp_test.c +++ b/tests/unit_tests/src/test/color_conversion_test.c @@ -12,14 +12,16 @@ #include "camera_main.h" -TEST_GROUP_RUNNER(isp_tests) { - RUN_TEST_CASE(isp_tests, yuv_to_rgb); - RUN_TEST_CASE(isp_tests, rgb_to_yuv); +#define print_separator(x) printf("\n---------- %s -------------\n", x) + +TEST_GROUP_RUNNER(color_conversion) { + RUN_TEST_CASE(color_conversion, yuv_to_rgb); + RUN_TEST_CASE(color_conversion, rgb_to_yuv); } -TEST_GROUP(isp_tests); -TEST_SETUP(isp_tests) { fflush(stdout); } -TEST_TEAR_DOWN(isp_tests) {} +TEST_GROUP(color_conversion); +TEST_SETUP(color_conversion) { fflush(stdout); print_separator("color_conversion");} +TEST_TEAR_DOWN(color_conversion) {} #define INV_DELTA 20 // error allowed in YUV RGB color conversion @@ -75,7 +77,7 @@ color_table_t ct_test_vector[num_tests] = { }; //TODO randomize this data -TEST(isp_tests, yuv_to_rgb) +TEST(color_conversion, yuv_to_rgb) { for(size_t i = 0; i < num_tests; i++) { @@ -96,7 +98,7 @@ TEST(isp_tests, yuv_to_rgb) } } -TEST(isp_tests, rgb_to_yuv) +TEST(color_conversion, rgb_to_yuv) { for(size_t i = 0; i < num_tests; i++) { diff --git a/tests/unit_tests/src/test/gamma_timing_test.c b/tests/unit_tests/src/test/gamma_timing_test.c new file mode 100644 index 00000000..89557dde --- /dev/null +++ b/tests/unit_tests/src/test/gamma_timing_test.c @@ -0,0 +1,75 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "unity_fixture.h" + +#include "isp.h" // gamma +#include "camera_utils.h" // time + +#define print_separator(x) printf("\n---------- %s -------------\n", x) + +// Unity +TEST_GROUP(gamma); +TEST_SETUP(gamma) { fflush(stdout); print_separator("gamma");} +TEST_TEAR_DOWN(gamma) {} +TEST_GROUP_RUNNER(gamma) { + RUN_TEST_CASE(gamma, gamma__basic); + RUN_TEST_CASE(gamma, gamma__double_size); +} + +// Test functions +void fill_array_rand_int8(uint8_t *image, size_t size){ + for(size_t idx = 0; idx < size; idx++){ + uint8_t random_number = (rand() % 256); + image[idx] = random_number; + } +} + +void test_gamma_size( + const char* func_name, + size_t height, + size_t width, + size_t channels) +{ + uint8_t image_buffer[channels][height][width]; + + // Seed the random number generator with the current time + srand(time(NULL)); + + // generate random numbers for the image buffer + size_t buffsize = height * width * channels; + fill_array_rand_int8((uint8_t *) &image_buffer[0][0][0], buffsize); + + // then measure and apply gamma + unsigned ts = measure_time(); + isp_gamma((uint8_t *) &image_buffer[0][0][0], gamma_new, height, width, channels); + unsigned tdiff = measure_time() - ts; + + // print info + printf("\tbuffsize: %d\n", buffsize); + PRINT_NAME_TIME(func_name, tdiff); +} + + +TEST(gamma, gamma__basic) +{ + static const char func_name[] = "gamma downsampled"; + const size_t height = APP_IMAGE_HEIGHT_PIXELS; + const size_t width = APP_IMAGE_WIDTH_PIXELS; + const size_t channels = APP_IMAGE_CHANNEL_COUNT; + test_gamma_size(func_name, height, width, channels); +} + +TEST(gamma, gamma__double_size) +{ + static const char func_name[] = "gamma double size"; + const size_t height = APP_IMAGE_HEIGHT_PIXELS*2; + const size_t width = APP_IMAGE_WIDTH_PIXELS*2; + const size_t channels = APP_IMAGE_CHANNEL_COUNT; + test_gamma_size(func_name, height, width, channels); +} From 548bc800ad440cdb477d773d4ac235eb31a5bbbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Fri, 23 Jun 2023 15:23:20 +0100 Subject: [PATCH 137/306] adding statistics tests --- camera/api/camera_utils.h | 2 +- tests/unit_tests/src/main.c | 3 +- tests/unit_tests/src/test/_helper.c | 15 ++++ tests/unit_tests/src/test/_helper.h | 12 +++ tests/unit_tests/src/test/gamma_timing_test.c | 27 +++--- tests/unit_tests/src/test/statistics_test.c | 83 +++++++++++++++++++ 6 files changed, 123 insertions(+), 19 deletions(-) create mode 100644 tests/unit_tests/src/test/_helper.c create mode 100644 tests/unit_tests/src/test/_helper.h create mode 100644 tests/unit_tests/src/test/statistics_test.c diff --git a/camera/api/camera_utils.h b/camera/api/camera_utils.h index 8c6ecf2d..e2e7195c 100644 --- a/camera/api/camera_utils.h +++ b/camera/api/camera_utils.h @@ -12,7 +12,7 @@ extern "C" { // Print macros #define PRINT_TIME(a,b) printf("%d\n", b - a); #define PRINT_NAME_TIME(name,time) \ - printf("\t%s timing: %u ticks, %.2fms\n", name, time, (float)time * 0.00001); + printf("\t%s timing: %u ticks, %.3fms\n", name, time, (float)time * 0.00001); /** * Measure the cpu ticks diff --git a/tests/unit_tests/src/main.c b/tests/unit_tests/src/main.c index bcbaa8cd..31f55ee9 100644 --- a/tests/unit_tests/src/main.c +++ b/tests/unit_tests/src/main.c @@ -19,7 +19,8 @@ int main( RUN_TEST_GROUP(pixel_hfilter); RUN_TEST_GROUP(pixel_vfilter); RUN_TEST_GROUP(color_conversion); - RUN_TEST_GROUP(gamma); + RUN_TEST_GROUP(gamma_timing); + RUN_TEST_GROUP(stats_test); return UNITY_END(); } diff --git a/tests/unit_tests/src/test/_helper.c b/tests/unit_tests/src/test/_helper.c new file mode 100644 index 00000000..b7e63fbe --- /dev/null +++ b/tests/unit_tests/src/test/_helper.c @@ -0,0 +1,15 @@ +#include "_helper.h" + +void fill_array_rand_uint8(uint8_t *image, size_t size){ + for(size_t idx = 0; idx < size; idx++){ + uint8_t random_number = (rand() % 256); + image[idx] = random_number; + } +} + +void fill_array_rand_int8(int8_t *image, size_t size){ + for(size_t idx = 0; idx < size; idx++){ + int8_t random_number = (rand() % 256) - 128; + image[idx] = random_number; + } +} diff --git a/tests/unit_tests/src/test/_helper.h b/tests/unit_tests/src/test/_helper.h new file mode 100644 index 00000000..6032d4b4 --- /dev/null +++ b/tests/unit_tests/src/test/_helper.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +#include + +// Print formatting +#define print_separator(x) printf("\n---------- %s -------------\n", x) + +// Fill array +void fill_array_rand_int8(int8_t *image, size_t size); +void fill_array_rand_uint8(uint8_t *image, size_t size); diff --git a/tests/unit_tests/src/test/gamma_timing_test.c b/tests/unit_tests/src/test/gamma_timing_test.c index 89557dde..42abc0ac 100644 --- a/tests/unit_tests/src/test/gamma_timing_test.c +++ b/tests/unit_tests/src/test/gamma_timing_test.c @@ -8,26 +8,19 @@ #include "unity_fixture.h" +#include "_helper.h" #include "isp.h" // gamma #include "camera_utils.h" // time #define print_separator(x) printf("\n---------- %s -------------\n", x) // Unity -TEST_GROUP(gamma); -TEST_SETUP(gamma) { fflush(stdout); print_separator("gamma");} -TEST_TEAR_DOWN(gamma) {} -TEST_GROUP_RUNNER(gamma) { - RUN_TEST_CASE(gamma, gamma__basic); - RUN_TEST_CASE(gamma, gamma__double_size); -} - -// Test functions -void fill_array_rand_int8(uint8_t *image, size_t size){ - for(size_t idx = 0; idx < size; idx++){ - uint8_t random_number = (rand() % 256); - image[idx] = random_number; - } +TEST_GROUP(gamma_timing); +TEST_SETUP(gamma_timing) { fflush(stdout); print_separator("gamma_timing");} +TEST_TEAR_DOWN(gamma_timing) {} +TEST_GROUP_RUNNER(gamma_timing) { + RUN_TEST_CASE(gamma_timing, gamma__basic); + RUN_TEST_CASE(gamma_timing, gamma__double_size); } void test_gamma_size( @@ -43,7 +36,7 @@ void test_gamma_size( // generate random numbers for the image buffer size_t buffsize = height * width * channels; - fill_array_rand_int8((uint8_t *) &image_buffer[0][0][0], buffsize); + fill_array_rand_uint8((uint8_t *) &image_buffer[0][0][0], buffsize); // then measure and apply gamma unsigned ts = measure_time(); @@ -56,7 +49,7 @@ void test_gamma_size( } -TEST(gamma, gamma__basic) +TEST(gamma_timing, gamma__basic) { static const char func_name[] = "gamma downsampled"; const size_t height = APP_IMAGE_HEIGHT_PIXELS; @@ -65,7 +58,7 @@ TEST(gamma, gamma__basic) test_gamma_size(func_name, height, width, channels); } -TEST(gamma, gamma__double_size) +TEST(gamma_timing, gamma__double_size) { static const char func_name[] = "gamma double size"; const size_t height = APP_IMAGE_HEIGHT_PIXELS*2; diff --git a/tests/unit_tests/src/test/statistics_test.c b/tests/unit_tests/src/test/statistics_test.c new file mode 100644 index 00000000..9aefa272 --- /dev/null +++ b/tests/unit_tests/src/test/statistics_test.c @@ -0,0 +1,83 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "unity_fixture.h" + +#include "_helper.h" +#include "statistics.h" +#include "camera_utils.h" // time + + +// Unity +TEST_GROUP(stats_test); +TEST_SETUP(stats_test) { fflush(stdout); print_separator("stats_test");} +TEST_TEAR_DOWN(stats_test) {} +TEST_GROUP_RUNNER(stats_test) { + RUN_TEST_CASE(stats_test, stats_test__basic); +} + + +TEST(stats_test, stats_test__basic){ + // create a random array + static const char func_name[] = "stats time"; + const size_t height = APP_IMAGE_HEIGHT_PIXELS; + const size_t width = APP_IMAGE_WIDTH_PIXELS; + const size_t channels = APP_IMAGE_CHANNEL_COUNT; + const size_t buffsize = height * width * channels; + + int8_t image_buffer[channels][height][width]; + fill_array_rand_int8((int8_t *) &image_buffer[0][0][0], buffsize); + + // create empty stats + global_stats_t global_stats = {{0}}; + + // compute histogram + unsigned ts = measure_time(); + unsigned tdiff_internal = 0; + + for (uint16_t h=0; h Date: Mon, 26 Jun 2023 09:43:18 +0100 Subject: [PATCH 138/306] programming guide section1 --- doc/1_programming_guide/01_Introduction.rst | 46 +++++++++++-------- ... 03_Building_and_testing_the_Software.rst} | 0 ....rst => 04_Developing_custom_software.rst} | 0 ...les.rst => 05_FAQ_and_Troubleshooting.rst} | 0 .../06_Troubleshooting.rst | 2 - doc/1_programming_guide/index.rst | 7 ++- doc/substitutions.rst | 3 ++ 7 files changed, 32 insertions(+), 26 deletions(-) rename doc/1_programming_guide/{03_Building_the_Software.rst => 03_Building_and_testing_the_Software.rst} (100%) rename doc/1_programming_guide/{04_Software_test_and_development.rst => 04_Developing_custom_software.rst} (100%) rename doc/1_programming_guide/{05_Examples.rst => 05_FAQ_and_Troubleshooting.rst} (100%) delete mode 100644 doc/1_programming_guide/06_Troubleshooting.rst diff --git a/doc/1_programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst index 3a28b8c7..dea96ca4 100644 --- a/doc/1_programming_guide/01_Introduction.rst +++ b/doc/1_programming_guide/01_Introduction.rst @@ -7,24 +7,6 @@ Overview --------- The purpose of this programming guide is to provide developers with a comprehensive understanding of the FWK_Camera architecture and guide them on how to effectively interact with cameras using the XCORE-AI-EXPLORER board. -The architecture consists of several key components that work together to facilitate camera communication and data processing. These components include: - -1. Camera hardware and camera interface -2. Camera drivers -3. User application -4. Image signal processing -5. I/O (Using Xscope_fileIO) - -Here below a high level block diagram of the FWK_Camera architecture: - -.. figure:: images/1_high_level_view.png - :alt: Alternate text for the image - :align: center - - High level block diagram of fwk camera. - - - Conventions and Terminology --------------------------- To ensure clarity and consistency throughout this guide, the following conventions and terminology are used: @@ -38,13 +20,37 @@ Features --------- - MIPI CSI2 interface - Up to 1GBps per lane -- Low resolution filtering -- Cameras supported: +- Low-resolution filtering +- Supported cameras: - IMX219 - GC2145 (explain hardware modification) +//TODO +The |EVK_BOARD| development board features an |X|-pin MIPI CSI2 port. +This port enables communication with cameras that are compatible with the |Xcore-AI| processor. +The processor is capable of directly processing an image from the sensor and performing various operations, +such as converting a RAW image to an RGB image (applying ISP functions), +analyzing the image using AI models with |xmos tools|, +converting a MIPI camera to USB interface, etc. + +This repository contains a set of tools for image acquisition, processing, and transmission. +The architecture, viewed from a high level, is composed of the following elements: + +.. figure:: images/1_high_level_view.png + :alt: Alternate text for the image + :align: center + + High-level block diagram of the FWK_Camera. + +1. Camera hardware and interface: The |tarjeta| board incorporates an MIPI connector and specific |X| hardware for transforming MIPI to |Y| ports. +2. Camera drivers: To process the image, we rely on the camera drivers, which provide a high-level API for image acquisition, filtering, and statistical analysis of the image. +3. Camera application: +4. Sensor configuration: +5. Camera output + Getting Started ---------------- + Hardware requirements: - XCORE.AI EVALUATION KIT (XK-EVK-XU316) diff --git a/doc/1_programming_guide/03_Building_the_Software.rst b/doc/1_programming_guide/03_Building_and_testing_the_Software.rst similarity index 100% rename from doc/1_programming_guide/03_Building_the_Software.rst rename to doc/1_programming_guide/03_Building_and_testing_the_Software.rst diff --git a/doc/1_programming_guide/04_Software_test_and_development.rst b/doc/1_programming_guide/04_Developing_custom_software.rst similarity index 100% rename from doc/1_programming_guide/04_Software_test_and_development.rst rename to doc/1_programming_guide/04_Developing_custom_software.rst diff --git a/doc/1_programming_guide/05_Examples.rst b/doc/1_programming_guide/05_FAQ_and_Troubleshooting.rst similarity index 100% rename from doc/1_programming_guide/05_Examples.rst rename to doc/1_programming_guide/05_FAQ_and_Troubleshooting.rst diff --git a/doc/1_programming_guide/06_Troubleshooting.rst b/doc/1_programming_guide/06_Troubleshooting.rst deleted file mode 100644 index 250cc8bf..00000000 --- a/doc/1_programming_guide/06_Troubleshooting.rst +++ /dev/null @@ -1,2 +0,0 @@ -Introduction -============= diff --git a/doc/1_programming_guide/index.rst b/doc/1_programming_guide/index.rst index 8d741e2e..a8e4348c 100644 --- a/doc/1_programming_guide/index.rst +++ b/doc/1_programming_guide/index.rst @@ -7,7 +7,6 @@ Programming Guide 01_Introduction 02_Architecture_and_Design - 03_Building_the_Software - 04_Software_test_and_development - 05_Examples - 06_Troubleshooting + 03_Building_and_testing_the_Software + 04_Developing_custom_software + 05_FAQ_and_Troubleshooting diff --git a/doc/substitutions.rst b/doc/substitutions.rst index 8f8b99c0..03c99287 100644 --- a/doc/substitutions.rst +++ b/doc/substitutions.rst @@ -5,3 +5,6 @@ .. _IMX219: https://www.opensourceinstruments.com/Electronics/Data/IMX219PQ.pdf .. _GH_FWK_CAMERA: https://github.com/xmos/fwk_camera .. _SW_TOOLS: https://www.xmos.ai/software-tools/ + + +.. EVK_BOARD: XCORE-AI-EXPLORER board From d1cfae4e930c74625f4d3cf6e4ddc9c6c919c0a6 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Mon, 26 Jun 2023 12:31:16 +0100 Subject: [PATCH 139/306] adding jenkinsfile --- Jenkinsfile | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 00000000..d9c2f898 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,58 @@ +@Library('xmos_jenkins_shared_library@v0.24.0') + +def runningOn(machine) { + println "Stage running on:" + println machine +} + +getApproval() +pipeline { + agent none + + parameters { + string( + name: 'TOOLS_VERSION', + defaultValue: '15.2.1', + description: 'The XTC tools version' + ) + } // parameters + + stages { + stage ('Build and Unit test') { + agent { + label 'linux&&x86_64' + } + stages { + + stage ('Build') { + steps { + runningOn(env.NODE_NAME) + // fetch submodules + sh 'git submodule update --init --recursive --jobs 4' + // build examples and tests + withTools(params.TOOLS_VERSION) { + sh 'cmake -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake' + sh 'make -C build -j4' + } + } + } // Build + + stage('Unit tests') { + steps { + dir('build/tests/unit_tests') { + withTools(params.TOOLS_VERSION) { + sh 'xsim --xscope "-offline trace.xmt" test_camera.xe' + } + } + } + } // Unit tests + + } // stages + post { + cleanup { + cleanWs() + } + } + } // Build and Unit test + } // stages +} // pipeline From 2a1623cc0c8f0b5b1f945b9d739dfd35df3c3d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 26 Jun 2023 15:00:43 +0100 Subject: [PATCH 140/306] converison from asm values --- camera/src/isp_functions.c | 4 +- tests/unit_tests/CMakeLists.txt | 2 + tests/unit_tests/api/_helpers.h | 33 +++++ tests/unit_tests/src/test/_helper.c | 15 -- tests/unit_tests/src/test/_helper.h | 12 -- tests/unit_tests/src/test/_helpers.c | 136 ++++++++++++++++++ .../src/test/color_conversion_test.c | 77 +++------- tests/unit_tests/src/test/gamma_timing_test.c | 5 +- tests/unit_tests/src/test/statistics_test.c | 11 +- 9 files changed, 201 insertions(+), 94 deletions(-) create mode 100644 tests/unit_tests/api/_helpers.h delete mode 100644 tests/unit_tests/src/test/_helper.c delete mode 100644 tests/unit_tests/src/test/_helper.h create mode 100644 tests/unit_tests/src/test/_helpers.c diff --git a/camera/src/isp_functions.c b/camera/src/isp_functions.c index 78732704..e80892b8 100644 --- a/camera/src/isp_functions.c +++ b/camera/src/isp_functions.c @@ -342,8 +342,8 @@ void isp_gamma( const size_t channels ) { - ssize_t buffsize = height * width * channels; - for(ssize_t idx = 0; idx < buffsize; idx++){ + size_t buffsize = height * width * channels; + for(size_t idx = 0; idx < buffsize; idx++){ uint8_t val = img_in[idx]; img_in[idx] = gamma_curve[val]; } diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index d206528e..b53cbc07 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -35,6 +35,8 @@ set(APP_COMPILE_DEFINITIONS XUD_CORE_CLOCK=600) +set(APP_INCLUDES api) + # ############################################################################## # Create executable # ############################################################################## diff --git a/tests/unit_tests/api/_helpers.h b/tests/unit_tests/api/_helpers.h new file mode 100644 index 00000000..1c2f25af --- /dev/null +++ b/tests/unit_tests/api/_helpers.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +// Print formatting +#define print_separator(x) printf("\n---------- %s -------------\n", x) + +// Common difinitions +#define CT_INT 127 // int conversion + +// Store the RGB color and corresponding values +typedef struct +{ + uint8_t R, G, B; + uint8_t Y, U, V; +} color_table_t; + +typedef enum{ + RGB_TO_YUV, + YUV_TO_RGB +} color_conversion_t; + + +// Fill array +void fill_array_rand_int8(int8_t *image, size_t size); +void fill_array_rand_uint8(uint8_t *image, size_t size); + +void fill_color_table_uint8(color_table_t table[], const size_t size, color_conversion_t conversion); +void printColorTable(color_table_t *table, uint8_t ref); +void yuv_to_rgb_ct(color_table_t *ct_ref, color_table_t *ct_res); +void rgb_to_yuv_ct(color_table_t *ct_ref, color_table_t *ct_res); diff --git a/tests/unit_tests/src/test/_helper.c b/tests/unit_tests/src/test/_helper.c deleted file mode 100644 index b7e63fbe..00000000 --- a/tests/unit_tests/src/test/_helper.c +++ /dev/null @@ -1,15 +0,0 @@ -#include "_helper.h" - -void fill_array_rand_uint8(uint8_t *image, size_t size){ - for(size_t idx = 0; idx < size; idx++){ - uint8_t random_number = (rand() % 256); - image[idx] = random_number; - } -} - -void fill_array_rand_int8(int8_t *image, size_t size){ - for(size_t idx = 0; idx < size; idx++){ - int8_t random_number = (rand() % 256) - 128; - image[idx] = random_number; - } -} diff --git a/tests/unit_tests/src/test/_helper.h b/tests/unit_tests/src/test/_helper.h deleted file mode 100644 index 6032d4b4..00000000 --- a/tests/unit_tests/src/test/_helper.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include -#include -#include - -// Print formatting -#define print_separator(x) printf("\n---------- %s -------------\n", x) - -// Fill array -void fill_array_rand_int8(int8_t *image, size_t size); -void fill_array_rand_uint8(uint8_t *image, size_t size); diff --git a/tests/unit_tests/src/test/_helpers.c b/tests/unit_tests/src/test/_helpers.c new file mode 100644 index 00000000..2fb64987 --- /dev/null +++ b/tests/unit_tests/src/test/_helpers.c @@ -0,0 +1,136 @@ +#include +#include + +#include "_helpers.h" +#include "camera_main.h" + +// Random generators +int8_t generate_rand_int8(){ + return (rand() % 256) - 128; +} +uint8_t generate_rand_uint8(){ + return (rand() % 256); +} + +// Single array fill functions +void fill_array_rand_uint8(uint8_t *image, const size_t size){ + for(size_t idx = 0; idx < size; idx++){ + image[idx] = generate_rand_uint8(); + } +} + +void fill_array_rand_int8(int8_t *image, const size_t size){ + for(size_t idx = 0; idx < size; idx++){ + image[idx] = generate_rand_int8(); + } +} + + +// Color conversion functions +typedef struct { + uint8_t y; + uint8_t u; + uint8_t v; +} YuvValues; + +YuvValues rgbToYuv(uint8_t r, uint8_t g, uint8_t b) { + float fr = (float)r; + float fg = (float)g; + float fb = (float)b; + + YuvValues yuv; + yuv.y = (uint8_t)(0.299f * fr + 0.587f * fg + 0.114f * fb + 0.5f); + yuv.u = (uint8_t)(-0.168736 * fr -0.331264 * fg + 0.500000 * fb + 128.0f); + yuv.v = (uint8_t)(0.500000 * fr -0.418688 * fg -0.081312 * fb + 128.0f); + + return yuv; +} + +typedef struct { + uint8_t r; + uint8_t g; + uint8_t b; +} RgbValues; + +RgbValues yuvToRgb(uint8_t y, uint8_t u, uint8_t v) { + RgbValues rgb; + + float fy = (float)y; + float fu = (float)(u - 128); + float fv = (float)(v - 128); + + float red = fy + 1.140625f * fv; + float green = fy - 0.396078 * fu - 0.584313 * fv; + float blue = fy + 2.0392156 * fu; + + rgb.r = (uint8_t)(red > 255.0f ? 255 : (red < 0.0f ? 0 : red)); + rgb.g = (uint8_t)(green > 255.0f ? 255 : (green < 0.0f ? 0 : green)); + rgb.b = (uint8_t)(blue > 255.0f ? 255 : (blue < 0.0f ? 0 : blue)); + + return rgb; +} + +void fill_color_table_uint8(color_table_t table[], const size_t size, color_conversion_t conversion){ + if (conversion == RGB_TO_YUV){ + for(size_t idx = 0; idx < size; idx++){ + table[idx].R = generate_rand_uint8(); + table[idx].G = generate_rand_uint8(); + table[idx].B = generate_rand_uint8(); + YuvValues yuv = rgbToYuv(table[idx].R, table[idx].G, table[idx].B); + table[idx].Y = yuv.y; + table[idx].U = yuv.u; + table[idx].V = yuv.v; + } + } + else if (conversion == YUV_TO_RGB){ + for(size_t idx = 0; idx < size; idx++){ + table[idx].Y = generate_rand_uint8(); + table[idx].U = generate_rand_uint8(); + table[idx].V = generate_rand_uint8(); + RgbValues rgb = yuvToRgb(table[idx].Y, table[idx].U, table[idx].V); + table[idx].R = rgb.r; + table[idx].G = rgb.g; + table[idx].B = rgb.b; + } + } + else{ + printf("Invalid color conversion type\n"); + assert(0); + } + + +} + +void printColorTable(color_table_t* table, uint8_t ref) { + if(ref) + { + printf("Expected "); + } + else + { + printf("Resulted "); + } + printf("Color Table:\n"); + printf("R: %d, G: %d, B: %d\n", table->R, table->G, table->B); + printf("Y: %d, U: %d, V: %d\n", table->Y, table->U, table->V); +} + +void yuv_to_rgb_ct(color_table_t* ct_ref, color_table_t* ct_res){ + ct_res -> Y = ct_ref -> Y; + ct_res -> U = ct_ref -> U; + ct_res -> V = ct_ref -> V; + uint32_t result = yuv_to_rgb(ct_ref->Y - CT_INT, ct_ref->U - CT_INT, ct_ref->V - CT_INT); + ct_res -> R = (uint8_t)(GET_R(result) + CT_INT); + ct_res -> G = (uint8_t)(GET_G(result) + CT_INT); + ct_res -> B = (uint8_t)(GET_B(result) + CT_INT); +} + +void rgb_to_yuv_ct(color_table_t* ct_ref, color_table_t* ct_res){ + ct_res -> R = ct_ref -> R; + ct_res -> G = ct_ref -> G; + ct_res -> B = ct_ref -> B; + uint32_t result = rgb_to_yuv(ct_ref->R - CT_INT, ct_ref->G - CT_INT, ct_ref->B - CT_INT); + ct_res -> Y = (uint8_t)(GET_Y(result) + CT_INT); + ct_res -> U = (uint8_t)(GET_U(result) + CT_INT); + ct_res -> V = (uint8_t)(GET_V(result) + CT_INT); +} diff --git a/tests/unit_tests/src/test/color_conversion_test.c b/tests/unit_tests/src/test/color_conversion_test.c index 10025ecb..74a0b61b 100644 --- a/tests/unit_tests/src/test/color_conversion_test.c +++ b/tests/unit_tests/src/test/color_conversion_test.c @@ -11,74 +11,29 @@ #include "unity_fixture.h" #include "camera_main.h" +#include "_helpers.h" -#define print_separator(x) printf("\n---------- %s -------------\n", x) +#define INV_DELTA 20 // error allowed in YUV RGB color conversion +#define num_tests 3 +// Define color conversion table and initialize random +color_table_t ct_test_vector[num_tests]; + +// Unity TEST_GROUP_RUNNER(color_conversion) { - RUN_TEST_CASE(color_conversion, yuv_to_rgb); - RUN_TEST_CASE(color_conversion, rgb_to_yuv); + RUN_TEST_CASE(color_conversion, conversion__yuv_to_rgb); + RUN_TEST_CASE(color_conversion, conversion__rgb_to_yuv); } - TEST_GROUP(color_conversion); TEST_SETUP(color_conversion) { fflush(stdout); print_separator("color_conversion");} TEST_TEAR_DOWN(color_conversion) {} - -#define INV_DELTA 20 // error allowed in YUV RGB color conversion -#define CT_INT 127 // int conversion - -// Store the RGB color and corresponding values -typedef struct +// Tests +TEST(color_conversion, conversion__yuv_to_rgb) { - int R, G, B; - int Y, U, V; -} color_table_t; - -void printColorTable(color_table_t* table, uint8_t ref) { - if(ref) - { - printf("Expected "); - } - else - { - printf("Resulted "); - } - printf("Color Table:\n"); - printf("R: %d, G: %d, B: %d\n", table->R, table->G, table->B); - printf("Y: %d, U: %d, V: %d\n", table->Y, table->U, table->V); -} + // initialize with random values + fill_color_table_uint8(&ct_test_vector[0], num_tests, YUV_TO_RGB); -void yuv_to_rgb_ct(color_table_t* ct_ref, color_table_t* ct_res){ - ct_res -> Y = ct_ref -> Y; - ct_res -> U = ct_ref -> U; - ct_res -> V = ct_ref -> V; - uint32_t result = yuv_to_rgb(ct_ref->Y - CT_INT, ct_ref->U - CT_INT, ct_ref->V - CT_INT); - ct_res -> R = (uint8_t)(GET_R(result) + CT_INT); - ct_res -> G = (uint8_t)(GET_G(result) + CT_INT); - ct_res -> B = (uint8_t)(GET_B(result) + CT_INT); -} - -void rgb_to_yuv_ct(color_table_t* ct_ref, color_table_t* ct_res){ - ct_res -> R = ct_ref -> R; - ct_res -> G = ct_ref -> G; - ct_res -> B = ct_ref -> B; - uint32_t result = rgb_to_yuv(ct_ref->R - CT_INT, ct_ref->G - CT_INT, ct_ref->B - CT_INT); - ct_res -> Y = (uint8_t)(GET_Y(result) + CT_INT); - ct_res -> U = (uint8_t)(GET_U(result) + CT_INT); - ct_res -> V = (uint8_t)(GET_V(result) + CT_INT); -} - -// R G B Y U V -#define num_tests 3 -color_table_t ct_test_vector[num_tests] = { - {48, 100, 16, 74, 94, 108}, - {192, 70, 23, 101, 83, 192}, - {58, 3, 156, 36, 195, 143} -}; -//TODO randomize this data - -TEST(color_conversion, yuv_to_rgb) -{ for(size_t i = 0; i < num_tests; i++) { // Define color table @@ -98,8 +53,12 @@ TEST(color_conversion, yuv_to_rgb) } } -TEST(color_conversion, rgb_to_yuv) + +TEST(color_conversion, conversion__rgb_to_yuv) { + // initialize with random values + fill_color_table_uint8(&ct_test_vector[0], num_tests, RGB_TO_YUV); + for(size_t i = 0; i < num_tests; i++) { // Define color table diff --git a/tests/unit_tests/src/test/gamma_timing_test.c b/tests/unit_tests/src/test/gamma_timing_test.c index 42abc0ac..347db247 100644 --- a/tests/unit_tests/src/test/gamma_timing_test.c +++ b/tests/unit_tests/src/test/gamma_timing_test.c @@ -8,12 +8,10 @@ #include "unity_fixture.h" -#include "_helper.h" +#include "_helpers.h" #include "isp.h" // gamma #include "camera_utils.h" // time -#define print_separator(x) printf("\n---------- %s -------------\n", x) - // Unity TEST_GROUP(gamma_timing); TEST_SETUP(gamma_timing) { fflush(stdout); print_separator("gamma_timing");} @@ -23,6 +21,7 @@ TEST_GROUP_RUNNER(gamma_timing) { RUN_TEST_CASE(gamma_timing, gamma__double_size); } +// Tests void test_gamma_size( const char* func_name, size_t height, diff --git a/tests/unit_tests/src/test/statistics_test.c b/tests/unit_tests/src/test/statistics_test.c index 9aefa272..bf0f3262 100644 --- a/tests/unit_tests/src/test/statistics_test.c +++ b/tests/unit_tests/src/test/statistics_test.c @@ -9,7 +9,7 @@ #include "unity_fixture.h" -#include "_helper.h" +#include "_helpers.h" #include "statistics.h" #include "camera_utils.h" // time @@ -25,7 +25,6 @@ TEST_GROUP_RUNNER(stats_test) { TEST(stats_test, stats_test__basic){ // create a random array - static const char func_name[] = "stats time"; const size_t height = APP_IMAGE_HEIGHT_PIXELS; const size_t width = APP_IMAGE_WIDTH_PIXELS; const size_t channels = APP_IMAGE_CHANNEL_COUNT; @@ -55,7 +54,7 @@ TEST(stats_test, stats_test__basic){ PRINT_NAME_TIME("time per histogram (all)", tdiff); // End of frame, compute statistics (order is important) - unsigned int ts0, ts1, ts2, ts3, ts4; + unsigned int ts0, ts1, ts2, ts3, ts4, total_time; uint8_t channel = 0; ts0 = measure_time(); stats_simple(&global_stats[channel]); @@ -67,6 +66,7 @@ TEST(stats_test, stats_test__basic){ stats_percentile_volume(&global_stats[channel]); ts4 = measure_time(); + total_time = ts4 - ts0; ts0 = ts1 - ts0; ts1 = ts2 - ts1; ts2 = ts3 - ts2; @@ -76,8 +76,13 @@ TEST(stats_test, stats_test__basic){ PRINT_NAME_TIME("time per stats_skewness", ts1); PRINT_NAME_TIME("time per stats_percentile", ts2); PRINT_NAME_TIME("time per stats_percentile_volume", ts3); + PRINT_NAME_TIME("time total", total_time); delay_milliseconds(100); printf("\n"); stats_print(&global_stats[channel], channel); + + // timing per channel has to meet time between frames 1/30s = 33ms + // time per stats*3 channels should be lower than 33ms + TEST_ASSERT_LESS_THAN_FLOAT(33 , total_time*3*0.00001); } From d0eac8437a219c616b84e0e3f1f7dc31b1ef2beb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 26 Jun 2023 15:36:14 +0100 Subject: [PATCH 141/306] including conversion as static --- tests/unit_tests/api/_helpers.h | 20 +++++++++++++++++--- tests/unit_tests/src/test/_helpers.c | 20 +++++--------------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/tests/unit_tests/api/_helpers.h b/tests/unit_tests/api/_helpers.h index 1c2f25af..261a2c8a 100644 --- a/tests/unit_tests/api/_helpers.h +++ b/tests/unit_tests/api/_helpers.h @@ -8,7 +8,7 @@ #define print_separator(x) printf("\n---------- %s -------------\n", x) // Common difinitions -#define CT_INT 127 // int conversion +#define CT_INT 127 // int conversion // Store the RGB color and corresponding values typedef struct @@ -17,13 +17,27 @@ typedef struct uint8_t Y, U, V; } color_table_t; -typedef enum{ +typedef enum +{ RGB_TO_YUV, YUV_TO_RGB } color_conversion_t; +typedef struct +{ + uint8_t y; + uint8_t u; + uint8_t v; +} YuvValues; + +typedef struct +{ + uint8_t r; + uint8_t g; + uint8_t b; +} RgbValues; -// Fill array +// ----------------------------------------------------- void fill_array_rand_int8(int8_t *image, size_t size); void fill_array_rand_uint8(uint8_t *image, size_t size); diff --git a/tests/unit_tests/src/test/_helpers.c b/tests/unit_tests/src/test/_helpers.c index 2fb64987..5062f2b7 100644 --- a/tests/unit_tests/src/test/_helpers.c +++ b/tests/unit_tests/src/test/_helpers.c @@ -27,12 +27,7 @@ void fill_array_rand_int8(int8_t *image, const size_t size){ // Color conversion functions -typedef struct { - uint8_t y; - uint8_t u; - uint8_t v; -} YuvValues; - +static YuvValues rgbToYuv(uint8_t r, uint8_t g, uint8_t b) { float fr = (float)r; float fg = (float)g; @@ -46,12 +41,7 @@ YuvValues rgbToYuv(uint8_t r, uint8_t g, uint8_t b) { return yuv; } -typedef struct { - uint8_t r; - uint8_t g; - uint8_t b; -} RgbValues; - +static RgbValues yuvToRgb(uint8_t y, uint8_t u, uint8_t v) { RgbValues rgb; @@ -59,9 +49,9 @@ RgbValues yuvToRgb(uint8_t y, uint8_t u, uint8_t v) { float fu = (float)(u - 128); float fv = (float)(v - 128); - float red = fy + 1.140625f * fv; - float green = fy - 0.396078 * fu - 0.584313 * fv; - float blue = fy + 2.0392156 * fu; + float red = fy + 1.1406f * fv; + float green = fy - 0.3960f * fu - 0.5843f * fv; + float blue = fy + 2.0392f * fu; rgb.r = (uint8_t)(red > 255.0f ? 255 : (red < 0.0f ? 0 : red)); rgb.g = (uint8_t)(green > 255.0f ? 255 : (green < 0.0f ? 0 : green)); From 0516116e960da55d12920ae0c3fc9f6d8bd7dce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 26 Jun 2023 16:20:11 +0100 Subject: [PATCH 142/306] deleting gamma double size --- tests/unit_tests/src/test/gamma_timing_test.c | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/unit_tests/src/test/gamma_timing_test.c b/tests/unit_tests/src/test/gamma_timing_test.c index 347db247..23940279 100644 --- a/tests/unit_tests/src/test/gamma_timing_test.c +++ b/tests/unit_tests/src/test/gamma_timing_test.c @@ -18,7 +18,6 @@ TEST_SETUP(gamma_timing) { fflush(stdout); print_separator("gamma_timing");} TEST_TEAR_DOWN(gamma_timing) {} TEST_GROUP_RUNNER(gamma_timing) { RUN_TEST_CASE(gamma_timing, gamma__basic); - RUN_TEST_CASE(gamma_timing, gamma__double_size); } // Tests @@ -56,12 +55,3 @@ TEST(gamma_timing, gamma__basic) const size_t channels = APP_IMAGE_CHANNEL_COUNT; test_gamma_size(func_name, height, width, channels); } - -TEST(gamma_timing, gamma__double_size) -{ - static const char func_name[] = "gamma double size"; - const size_t height = APP_IMAGE_HEIGHT_PIXELS*2; - const size_t width = APP_IMAGE_WIDTH_PIXELS*2; - const size_t channels = APP_IMAGE_CHANNEL_COUNT; - test_gamma_size(func_name, height, width, channels); -} From 55e9674fa56d1a5f8bb14b6596d929a8dc077c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 26 Jun 2023 16:50:34 +0100 Subject: [PATCH 143/306] adding K factor for image, and deleting delay --- tests/unit_tests/api/_helpers.h | 3 +++ tests/unit_tests/src/test/gamma_timing_test.c | 4 ++-- tests/unit_tests/src/test/statistics_test.c | 7 ++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/unit_tests/api/_helpers.h b/tests/unit_tests/api/_helpers.h index 261a2c8a..b49690e8 100644 --- a/tests/unit_tests/api/_helpers.h +++ b/tests/unit_tests/api/_helpers.h @@ -4,6 +4,9 @@ #include #include +// Decimator factor for tests +#define K 4 // downsampled image / 4*4 + // Print formatting #define print_separator(x) printf("\n---------- %s -------------\n", x) diff --git a/tests/unit_tests/src/test/gamma_timing_test.c b/tests/unit_tests/src/test/gamma_timing_test.c index 23940279..21c4f352 100644 --- a/tests/unit_tests/src/test/gamma_timing_test.c +++ b/tests/unit_tests/src/test/gamma_timing_test.c @@ -50,8 +50,8 @@ void test_gamma_size( TEST(gamma_timing, gamma__basic) { static const char func_name[] = "gamma downsampled"; - const size_t height = APP_IMAGE_HEIGHT_PIXELS; - const size_t width = APP_IMAGE_WIDTH_PIXELS; + const size_t height = APP_IMAGE_HEIGHT_PIXELS / K; + const size_t width = APP_IMAGE_WIDTH_PIXELS / K; const size_t channels = APP_IMAGE_CHANNEL_COUNT; test_gamma_size(func_name, height, width, channels); } diff --git a/tests/unit_tests/src/test/statistics_test.c b/tests/unit_tests/src/test/statistics_test.c index bf0f3262..6440714e 100644 --- a/tests/unit_tests/src/test/statistics_test.c +++ b/tests/unit_tests/src/test/statistics_test.c @@ -13,7 +13,6 @@ #include "statistics.h" #include "camera_utils.h" // time - // Unity TEST_GROUP(stats_test); TEST_SETUP(stats_test) { fflush(stdout); print_separator("stats_test");} @@ -25,8 +24,8 @@ TEST_GROUP_RUNNER(stats_test) { TEST(stats_test, stats_test__basic){ // create a random array - const size_t height = APP_IMAGE_HEIGHT_PIXELS; - const size_t width = APP_IMAGE_WIDTH_PIXELS; + const size_t height = APP_IMAGE_HEIGHT_PIXELS / K; + const size_t width = APP_IMAGE_WIDTH_PIXELS / K; const size_t channels = APP_IMAGE_CHANNEL_COUNT; const size_t buffsize = height * width * channels; @@ -78,8 +77,6 @@ TEST(stats_test, stats_test__basic){ PRINT_NAME_TIME("time per stats_percentile_volume", ts3); PRINT_NAME_TIME("time total", total_time); - delay_milliseconds(100); - printf("\n"); stats_print(&global_stats[channel], channel); // timing per channel has to meet time between frames 1/30s = 33ms From 18a126120bc3594857dc741df0f605f5fe86761b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 26 Jun 2023 17:13:26 +0100 Subject: [PATCH 144/306] including print.h --- camera/api/camera_utils.h | 11 +++++++++ camera/src/statistics.c | 26 +++++++++++++-------- tests/unit_tests/src/test/statistics_test.c | 14 +++++------ 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/camera/api/camera_utils.h b/camera/api/camera_utils.h index e2e7195c..cc16052a 100644 --- a/camera/api/camera_utils.h +++ b/camera/api/camera_utils.h @@ -3,6 +3,8 @@ #include #include +#include "print.h" + #include "sensor.h" #if defined(__XC__) || defined(__cplusplus) @@ -37,6 +39,15 @@ void vect_int8_to_uint8( int8_t input[], const unsigned length); + +inline void fast_print_name_time(const char * name, unsigned time){ + const char* formattedString = "\t%s timing: %u ticks, %.3fms\n"; + char output[255]; // Assuming a maximum length for the formatted string + sprintf(output, formattedString, name, time, (float)time * 0.00001); + printstr(output); + //printf("\t%s timing: %u ticks, %.3fms\n", name, time, (float)time * 0.00001); +} + #if defined(__XC__) || defined(__cplusplus) } #endif diff --git a/camera/src/statistics.c b/camera/src/statistics.c index d59b0bbd..fca1bca4 100644 --- a/camera/src/statistics.c +++ b/camera/src/statistics.c @@ -2,6 +2,7 @@ #include "statistics.h" #include "isp.h" +#include "print.h" // Number of samples taken each row #define HISTOGRAM_SAMPLE_PER_ROW ((APP_IMAGE_WIDTH_PIXELS + APP_HISTOGRAM_SAMPLE_STEP - 1) / (APP_HISTOGRAM_SAMPLE_STEP)) @@ -86,16 +87,21 @@ void stats_simple(channel_stats_t *stats) void stats_print(channel_stats_t *stats, unsigned channel){ - printf("ch:%d,Mi:%d,Ma:%d,Mean:%f,Sk:%f,pct:%d,mi_c:%lu,ma_c:%lu,pc:%lu\n", - channel, - stats->min, - stats->max, - stats->mean, - stats->skewness, - stats->percentile, - stats->min_count, - stats->max_count, - stats->per_count); + const char* formattedString = "\nch:%d,Mi:%d,Ma:%d,Mean:%f,Sk:%f,pct:%d,mi_c:%lu,ma_c:%lu,pc:%lu\n"; + char output[255]; // Assuming a maximum length for the formatted string + sprintf(output, formattedString, + channel, + stats->min, + stats->max, + stats->mean, + stats->skewness, + stats->percentile, + stats->min_count, + stats->max_count, + stats->per_count); + + printstr(output); + // printf("%s", output); } diff --git a/tests/unit_tests/src/test/statistics_test.c b/tests/unit_tests/src/test/statistics_test.c index 6440714e..f786af6c 100644 --- a/tests/unit_tests/src/test/statistics_test.c +++ b/tests/unit_tests/src/test/statistics_test.c @@ -49,8 +49,8 @@ TEST(stats_test, stats_test__basic){ tdiff_internal = measure_time() - ts_internal; } unsigned tdiff = measure_time() - ts; - PRINT_NAME_TIME("time per histogram (row)", tdiff_internal); - PRINT_NAME_TIME("time per histogram (all)", tdiff); + fast_print_name_time("time per histogram (row)", tdiff_internal); + fast_print_name_time("time per histogram (all)", tdiff); // End of frame, compute statistics (order is important) unsigned int ts0, ts1, ts2, ts3, ts4, total_time; @@ -71,11 +71,11 @@ TEST(stats_test, stats_test__basic){ ts2 = ts3 - ts2; ts3 = ts4 - ts3; - PRINT_NAME_TIME("time per stats_simple", ts0); - PRINT_NAME_TIME("time per stats_skewness", ts1); - PRINT_NAME_TIME("time per stats_percentile", ts2); - PRINT_NAME_TIME("time per stats_percentile_volume", ts3); - PRINT_NAME_TIME("time total", total_time); + fast_print_name_time("time per stats_simple", ts0); + fast_print_name_time("time per stats_skewness", ts1); + fast_print_name_time("time per stats_percentile", ts2); + fast_print_name_time("time per stats_percentile_volume", ts3); + fast_print_name_time("time total", total_time); stats_print(&global_stats[channel], channel); From 17628c8930db9e44e730762ecfd3cc11ee7119ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Tue, 27 Jun 2023 11:31:16 +0100 Subject: [PATCH 145/306] minor awb changes, test vpu vs non vpu color conv --- camera/api/camera_utils.h | 13 +--- camera/api/isp.h | 8 --- camera/src/isp_functions.c | 63 ++++++------------- camera/src/isp_pipeline.c | 4 +- sensors/api/sensor.h | 4 +- .../raw_vs_downsampled/test_downs_vs_raw.py | 4 +- tests/unit_tests/api/_helpers.h | 3 + tests/unit_tests/src/test/_helpers.c | 2 - .../src/test/color_conversion_test.c | 40 ++++++++++++ tests/unit_tests/src/test/statistics_test.c | 14 ++--- 10 files changed, 75 insertions(+), 80 deletions(-) diff --git a/camera/api/camera_utils.h b/camera/api/camera_utils.h index cc16052a..7b75deb4 100644 --- a/camera/api/camera_utils.h +++ b/camera/api/camera_utils.h @@ -3,8 +3,6 @@ #include #include -#include "print.h" - #include "sensor.h" #if defined(__XC__) || defined(__cplusplus) @@ -14,7 +12,7 @@ extern "C" { // Print macros #define PRINT_TIME(a,b) printf("%d\n", b - a); #define PRINT_NAME_TIME(name,time) \ - printf("\t%s timing: %u ticks, %.3fms\n", name, time, (float)time * 0.00001); + printf("\t%s: %u ticks, %.3fms\n", name, time, (float)time * 0.00001); /** * Measure the cpu ticks @@ -39,15 +37,6 @@ void vect_int8_to_uint8( int8_t input[], const unsigned length); - -inline void fast_print_name_time(const char * name, unsigned time){ - const char* formattedString = "\t%s timing: %u ticks, %.3fms\n"; - char output[255]; // Assuming a maximum length for the formatted string - sprintf(output, formattedString, name, time, (float)time * 0.00001); - printstr(output); - //printf("\t%s timing: %u ticks, %.3fms\n", name, time, (float)time * 0.00001); -} - #if defined(__XC__) || defined(__cplusplus) } #endif diff --git a/camera/api/isp.h b/camera/api/isp.h index e9e11e96..03dd6137 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -133,14 +133,6 @@ extern const uint8_t gamma_1p8_s1[256]; extern const uint8_t gamma_1p4_s1[256]; extern const uint8_t gamma_new[256]; -/** - * @brief gamma correction function - * - * @param buffsize size of the image buffer - * @param img pointer to the image buffer - */ -void isp_gamma_stride1(const uint32_t buffsize, uint8_t *img); - /** * @brief compute gamma curve given an image and the gamma adjustement * diff --git a/camera/src/isp_functions.c b/camera/src/isp_functions.c index e80892b8..27d7b9d3 100644 --- a/camera/src/isp_functions.c +++ b/camera/src/isp_functions.c @@ -1,4 +1,8 @@ #include "isp.h" + +#include +#include + #define INCLUDE_ABS 0 // ---------------------------------- utils ------------------------------ @@ -21,7 +25,7 @@ uint8_t AE_control_exposure( // Initial exposure static uint8_t new_exp = AE_INITIAL_EXPOSURE; static uint8_t printf_info = 1; - static uint8_t give_up = 0; + static uint8_t skip_ae_control = 0; // if too dark for a ceertain frames, skip AE control // Compute skewness and adjust exposure if needed float sk = AE_compute_mean_skewness(global_stats); @@ -36,11 +40,10 @@ uint8_t AE_control_exposure( new_exp = AE_compute_new_exposure((float)new_exp, sk); sensor_control_set_exposure(sc_if, (uint8_t)new_exp); printf_info = 1; - // if it is too dark, give up but continue with awb if (new_exp > 70){ - give_up++; - if (give_up > 5){ - give_up = 0; + skip_ae_control++; + if (skip_ae_control > 5){ + skip_ae_control = 0; return 1; } } @@ -134,32 +137,7 @@ void AWB_compute_gains_percentile(global_stats_t *gstats, isp_params_t *isp_para tmp0 = green_p/(float)red_p; tmp1 = 1; tmp2 = green_p/(float)blue_p; - - //tmp0 = 255.0 / (float)(*gstats)[0].percentile; // RED - // tmp1 = 255.0 / (float)(*gstats)[1].percentile; // GREEN - //tmp2 = 255.0 / (float)(*gstats)[2].percentile; // BLUE - - /* - // add skewness contribution - const float skmin = -1; - const float skmax = 1; - const float gmin = 1; - const float gmax = 1.5; - const float grange = gmax - gmin; - const float skrange = skmax - skmin; - const float m = -(grange/skrange); - - float gains[3]; - - for (int i=0; i< 3; i++){ - float sk = (*gstats)[i].skewness; - gains[i] = m*(sk - skmin) + gmax; - } - - tmp0 = 0.7*tmp0 + 0.3*gains[0]; - tmp1 = 0.7*tmp1 + 0.3*gains[1]; - tmp2 = 0.7*tmp2 + 0.3*gains[2]; - */ + uint32_t r_per_count = (*gstats)[0].per_count; uint32_t g_per_count = (*gstats)[1].per_count; uint32_t b_per_count = (*gstats)[2].per_count; @@ -274,6 +252,7 @@ void AWB_print_gains(isp_params_t *isp_params){ // ---------------------------------- GAMMA ------------------------------ + const uint8_t gamma_1p8_s1[256] = { 0,12,17,22,25,29,32,35,37,40,42,44,47,49,51,53,55,57,58,60,62,64,65,67,69, 70,72,73,75,76,78,79,80,82,83,85,86,87,89,90,91,92,94,95,96,97,98,100,101, @@ -289,7 +268,6 @@ const uint8_t gamma_1p8_s1[256] = { 235,236,236,237,237,238,238,239,240,240,241,241,242,243,243,244,244,245,245, 246,247,247,247,246,245,244,243,242,241,240,239,238,237,236,235,236}; - const uint8_t gamma_1p4_s1[256] = { 0,0,0,1,3,5,8,10,12,13,15,17,19,20,22,24,25,27,28,30,31,33,34,36,37,39, 40,41,43,44,45,47,48,49,50,52,53,54,55,57,58,59,60,62,63,64,65,66,67,68,70,71, @@ -302,7 +280,6 @@ const uint8_t gamma_1p4_s1[256] = { 210,211,212,213,213,214,215,216,216,217,218,219,219,220,221,222,222,223,224,225,225,226,227,228,228,229, 230,231,231,232,233,233,234,235,236,236,237,238,239,239,240,241,241,242,243,244,245,247}; - // gamma 1.8, with substract 10 and 1.05 multiplier const uint8_t gamma_new[256] = { 0, 0, 0, 0, 0, 0, 0, 2, 5, 8, 11, 14, 16, @@ -324,15 +301,8 @@ const uint8_t gamma_new[256] = { 225, 226, 227, 227, 228, 229, 229, 230, 231, 232, 232, 233, 234, 234, 235, 236, 236, 237, 238, 238, 239, 240, 240, 241, 242, 242, 243, 244, 244, 245, 246, 246, 247, 248, 248, 249, 250, 250, 251, -252, 252, 253, 254, 254, 255, 255, 255}; +252, 252, 253, 254, 254, 255, 255, 255, 255}; -void isp_gamma_stride1(const uint32_t buffsize, uint8_t *img){ - // gamma naming: 1p8_s1 = gamma 1.8 , with a stride of 1 - // 1p8_s4 => img^(1/1.8) (in a normalizeed 0-1 image) - for(uint32_t i=0; i Date: Tue, 27 Jun 2023 11:45:16 +0100 Subject: [PATCH 146/306] removing BL comment, gamma original function --- camera/src/isp_functions.c | 7 ++----- sensors/api/sensor.h | 5 ----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/camera/src/isp_functions.c b/camera/src/isp_functions.c index 27d7b9d3..78760f67 100644 --- a/camera/src/isp_functions.c +++ b/camera/src/isp_functions.c @@ -314,11 +314,8 @@ void isp_gamma( { assert(gamma_curve[255] != 0); // ensure all values are filles up size_t buffsize = height * width * channels; - for(size_t idx = 0; idx < buffsize; idx+=4){ - img_in[idx] = gamma_curve[img_in[idx]]; - img_in[idx+1] = gamma_curve[img_in[idx+1]]; - img_in[idx+2] = gamma_curve[img_in[idx+2]]; - img_in[idx+3] = gamma_curve[img_in[idx+3]]; + for(size_t idx = 0; idx < buffsize; idx++){ + img_in[idx] = gamma_curve[img_in[idx]]; } } diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index fd928c0f..226fd5c1 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -34,11 +34,6 @@ #define sensor_configure(iic) imx219_configure(iic) #define sensor_set_exposure(iic,ex) imx219_set_exposure(iic,ex) #define SENSOR_BLACK_LEVEL 16 - //#define SENSOR_BLACK_LEVEL 32 // we intentionally set it to 32 increase dynamic range with gamma - //TODO - // ideally we should have the sensor black_level here that does not change and then - // we can have a custom black level that we set in isp.h - // example: #define CUSTOM_BLACK_LEVEL SENSOR_BLACK_LEVEL + 20 #endif #if CONFIG_GC2145_SUPPORT From 782a2e2a1fef631f4afddb4107535c2907d0be24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Tue, 27 Jun 2023 13:17:33 +0100 Subject: [PATCH 147/306] High level structure --- .gitignore | 1 + camera/src/camera_api.c | 21 ++++++------ doc/1_programming_guide/01_Introduction.rst | 26 ++++++++------ doc/2_quick_start_guide/01_Introduction.rst | 4 --- doc/2_quick_start_guide/Quick_Start_guide.rst | 34 +++++++++++++++++++ doc/2_quick_start_guide/index.rst | 2 +- doc/{2_user_guide => 3_user_guide}/.gitkeep | 0 doc/3_user_guide/User_guide.rst | 2 ++ doc/3_user_guide/index.rst | 8 +++++ .../.gitkeep | 0 .../index.rst | 0 .../release_notes.rst | 0 doc/index.rst | 3 +- 13 files changed, 73 insertions(+), 28 deletions(-) delete mode 100644 doc/2_quick_start_guide/01_Introduction.rst create mode 100644 doc/2_quick_start_guide/Quick_Start_guide.rst rename doc/{2_user_guide => 3_user_guide}/.gitkeep (100%) create mode 100644 doc/3_user_guide/User_guide.rst create mode 100644 doc/3_user_guide/index.rst rename doc/{3_release_notes => 4_release_notes}/.gitkeep (100%) rename doc/{3_release_notes => 4_release_notes}/index.rst (100%) rename doc/{3_release_notes => 4_release_notes}/release_notes.rst (100%) diff --git a/.gitignore b/.gitignore index b7492ee0..e74220b6 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ *.bin *.fs *.exe +.editorconfig # Test cruft **/.pytest_cache/* diff --git a/camera/src/camera_api.c b/camera/src/camera_api.c index 5a4c2ccc..d4d4fbba 100644 --- a/camera/src/camera_api.c +++ b/camera/src/camera_api.c @@ -43,8 +43,7 @@ void camera_stop(){ void camera_new_row( const int8_t pixel_data[W_RAW], - const unsigned row_index) -{ + const unsigned row_index){ int8_t* user_pixel_data; SELECT_RES( @@ -65,18 +64,18 @@ void camera_new_row_decimated( const int8_t pixel_data[CH][W], const unsigned row_index) { - int8_t* user_pixel_data; + int8_t *user_pixel_data; - SELECT_RES( - CASE_THEN(c_user_api[CHAN_DEC].end_a, user_handler), - DEFAULT_THEN(default_handler)) + SELECT_RES( + CASE_THEN(c_user_api[CHAN_DEC].end_a, user_handler), + DEFAULT_THEN(default_handler)) { - user_handler: - user_pixel_data = (int8_t*) s_chan_in_word(c_user_api[CHAN_DEC].end_a); - memcpy(user_pixel_data, (void*) pixel_data, CH*W); + user_handler: + user_pixel_data = (int8_t *)s_chan_in_word(c_user_api[CHAN_DEC].end_a); + memcpy(user_pixel_data, (void *)pixel_data, CH * W); s_chan_out_word(c_user_api[CHAN_DEC].end_a, row_index); break; - default_handler: + default_handler: break; } } @@ -86,7 +85,7 @@ unsigned camera_capture_row( { s_chan_out_word(c_user_api[CHAN_RAW].end_b, (unsigned) &pixel_data[0]); unsigned sdf = s_chan_in_word(c_user_api[CHAN_RAW].end_b); - return sdf; + return sdf; } diff --git a/doc/1_programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst index dea96ca4..7be995de 100644 --- a/doc/1_programming_guide/01_Introduction.rst +++ b/doc/1_programming_guide/01_Introduction.rst @@ -5,7 +5,7 @@ Introduction Overview --------- -The purpose of this programming guide is to provide developers with a comprehensive understanding of the FWK_Camera architecture and guide them on how to effectively interact with cameras using the XCORE-AI-EXPLORER board. +The purpose of this programming guide is to provide developers with a comprehensive understanding of the FWK_Camera architecture and guide them on how to effectively interact with cameras using the |XCORE-AI-EXPLORER| board. Conventions and Terminology --------------------------- @@ -25,13 +25,12 @@ Features - IMX219 - GC2145 (explain hardware modification) -//TODO -The |EVK_BOARD| development board features an |X|-pin MIPI CSI2 port. -This port enables communication with cameras that are compatible with the |Xcore-AI| processor. -The processor is capable of directly processing an image from the sensor and performing various operations, + +The |XCORE-AI-EXPLORER| board features an 15-pin MIPI CSI2 port (compatible with Raspberry PI). +This port is connected to the |Xcore-AI| processor, so we can directly processing an image from an external sensor and performing various operations, such as converting a RAW image to an RGB image (applying ISP functions), -analyzing the image using AI models with |xmos tools|, -converting a MIPI camera to USB interface, etc. +analyzing the image using AI models with |xmos-ai-tools|, +converting a MIPI camera to other interfaces as USB, SPI, etc. This repository contains a set of tools for image acquisition, processing, and transmission. The architecture, viewed from a high level, is composed of the following elements: @@ -42,15 +41,20 @@ The architecture, viewed from a high level, is composed of the following element High-level block diagram of the FWK_Camera. -1. Camera hardware and interface: The |tarjeta| board incorporates an MIPI connector and specific |X| hardware for transforming MIPI to |Y| ports. -2. Camera drivers: To process the image, we rely on the camera drivers, which provide a high-level API for image acquisition, filtering, and statistical analysis of the image. -3. Camera application: -4. Sensor configuration: +1. Camera hardware and interface +2. Camera drivers +3. Camera application +4. Sensor configuration 5. Camera output +Each of these elements is described in detail in the following sections. + Getting Started ---------------- +To start using the FWK_Camera, you can proceed to the Quick Start Guide: `QS_FWKC`_. + + Hardware requirements: - XCORE.AI EVALUATION KIT (XK-EVK-XU316) diff --git a/doc/2_quick_start_guide/01_Introduction.rst b/doc/2_quick_start_guide/01_Introduction.rst deleted file mode 100644 index 600cfb0d..00000000 --- a/doc/2_quick_start_guide/01_Introduction.rst +++ /dev/null @@ -1,4 +0,0 @@ -Intro quick -============= - -loremp ipsum diff --git a/doc/2_quick_start_guide/Quick_Start_guide.rst b/doc/2_quick_start_guide/Quick_Start_guide.rst new file mode 100644 index 00000000..ed58da6f --- /dev/null +++ b/doc/2_quick_start_guide/Quick_Start_guide.rst @@ -0,0 +1,34 @@ +Getting Started +---------------- + +Hardware requirements: +^^^^^^^^^^^^^^^^^^^^^^^ +- XCORE.AI EVALUATION KIT (XK-EVK-XU316) +- Camera module +- Power supply +- Micro USB cable +- JTAG debugger + +Software requirements: +^^^^^^^^^^^^^^^^^^^^^^^ +- XMOS tools: `SW_TOOLS`_ +- Xscope FIleio: `xscope_fileio`_ +- CMake, Ninja (Windows) +- Python 3.7 or later + + +Run the RAW camera demo +^^^^^^^^^^^^^^^^^^^^^^^ +This demo uses the RAW camera module to capture a RAW8 image and save it to a .raw file. +Then this image can be decoded using the `RAW image decoder`_. + +1. Make sure that the camera is connecte to the board +2. Connect Power Supply and JTAG debugger +3. Build he example using the following commands: + a. `cmake -G "Ninja" -DTOOLCHAIN=xmos_toolchain -B build -S .` + b. `ninja -C build example_take_picture_raw` +4. Run the example using the following command: + a. `python python/run_xscope_bin.py build/examples/take_picture_raw/example_take_picture_raw.xe` +5. You should see the camera comminucating with the host and the image being saved to a .raw file +6. To decode the image use the `RAW image decoder`_ and the following command: + a. `python decode_raw8.py` diff --git a/doc/2_quick_start_guide/index.rst b/doc/2_quick_start_guide/index.rst index 5c93d536..d73e8414 100644 --- a/doc/2_quick_start_guide/index.rst +++ b/doc/2_quick_start_guide/index.rst @@ -5,4 +5,4 @@ Quick start guide .. toctree:: :maxdepth: 1 - 01_Introduction + Quick_Start_guide diff --git a/doc/2_user_guide/.gitkeep b/doc/3_user_guide/.gitkeep similarity index 100% rename from doc/2_user_guide/.gitkeep rename to doc/3_user_guide/.gitkeep diff --git a/doc/3_user_guide/User_guide.rst b/doc/3_user_guide/User_guide.rst new file mode 100644 index 00000000..4d07e2c7 --- /dev/null +++ b/doc/3_user_guide/User_guide.rst @@ -0,0 +1,2 @@ +User guide +---------------- diff --git a/doc/3_user_guide/index.rst b/doc/3_user_guide/index.rst new file mode 100644 index 00000000..05a76268 --- /dev/null +++ b/doc/3_user_guide/index.rst @@ -0,0 +1,8 @@ +******************* +User guide +******************* + +.. toctree:: + :maxdepth: 1 + + User_guide diff --git a/doc/3_release_notes/.gitkeep b/doc/4_release_notes/.gitkeep similarity index 100% rename from doc/3_release_notes/.gitkeep rename to doc/4_release_notes/.gitkeep diff --git a/doc/3_release_notes/index.rst b/doc/4_release_notes/index.rst similarity index 100% rename from doc/3_release_notes/index.rst rename to doc/4_release_notes/index.rst diff --git a/doc/3_release_notes/release_notes.rst b/doc/4_release_notes/release_notes.rst similarity index 100% rename from doc/3_release_notes/release_notes.rst rename to doc/4_release_notes/release_notes.rst diff --git a/doc/index.rst b/doc/index.rst index 738952c6..3f808c15 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -9,4 +9,5 @@ Framework of camera processing libraries for XCORE.AI. ./1_programming_guide/index ./2_quick_start_guide/index - ./3_release_notes/index + ./3_user_guide/index + ./4_release_notes/index From 8d0aa28511fb3c91bc7f4470f01131625dc8c33d Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 27 Jun 2023 14:28:31 +0100 Subject: [PATCH 148/306] styling, API celanup, warnings --- camera/CMakeLists.txt | 2 ++ camera/api/camera_api.h | 2 -- camera/api/camera_main.h | 5 --- camera/api/camera_utils.h | 2 -- camera/api/image_hfilter.h | 5 --- camera/api/image_vfilter.h | 10 ++---- camera/api/isp.h | 12 ++----- camera/api/packet_handler.h | 10 +----- camera/api/statistics.h | 8 +---- camera/src/camera_api.c | 7 ---- camera/src/camera_main.xc | 4 --- camera/src/camera_utils.c | 6 ++-- camera/src/image_hfilter.c | 3 -- camera/src/image_vfilter.c | 3 -- camera/src/isp_functions.c | 15 ++------ camera/src/isp_pipeline.c | 1 - camera/src/packet_handler.c | 22 ++---------- camera/src/statistics.c | 5 --- .../take_picture_downsample/CMakeLists.txt | 33 ++++++------------ examples/take_picture_downsample/src/app.c | 2 -- examples/take_picture_raw/CMakeLists.txt | 34 ++++++------------- modules/i2c.cmake | 24 ++++++------- modules/mipi/CMakeLists.txt | 9 +++-- modules/xassert.cmake | 8 ++--- sensors/CMakeLists.txt | 5 +++ sensors/_galaxycore_gc2145/gc2145.xc | 2 +- sensors/api/sensor.h | 12 +------ sensors/api/sensor_control.h | 1 - sensors/src/sensor_control.xc | 2 -- .../hardware_tests/test_timing/CMakeLists.txt | 10 +++--- tests/unit_tests/CMakeLists.txt | 12 ++----- tests/unity.cmake | 24 ++++++++----- utils/io_utils/io_utils.c | 1 - 33 files changed, 84 insertions(+), 217 deletions(-) diff --git a/camera/CMakeLists.txt b/camera/CMakeLists.txt index c1221af9..affea754 100644 --- a/camera/CMakeLists.txt +++ b/camera/CMakeLists.txt @@ -30,6 +30,8 @@ target_sources(${LIB_NAME} target_compile_options(${LIB_NAME} PUBLIC -Os + -Wall + -Werror -g -fxscope -mcmodel=large ) diff --git a/camera/api/camera_api.h b/camera/api/camera_api.h index da8c3cfa..f7892127 100644 --- a/camera/api/camera_api.h +++ b/camera/api/camera_api.h @@ -89,7 +89,6 @@ unsigned camera_capture_row_decimated( unsigned camera_capture_image_raw( int8_t image_buff[H_RAW][W_RAW]); - /** * CLIENT SIDE * @@ -102,7 +101,6 @@ unsigned camera_capture_image_raw( unsigned camera_capture_image( int8_t image_buff[CH][H][W]); - typedef struct { struct { unsigned row; diff --git a/camera/api/camera_main.h b/camera/api/camera_main.h index b6707e62..ccbcc197 100644 --- a/camera/api/camera_main.h +++ b/camera/api/camera_main.h @@ -6,10 +6,8 @@ #include "i2c.h" #include "mipi.h" - #include "sensor.h" - #ifndef MIPI_CLKBLK #define MIPI_CLKBLK XS1_CLKBLK_1 #endif @@ -24,7 +22,6 @@ #include "camera_utils.h" #include "isp.h" - // MIPI Shim configuration register layout (MIPI_SHIM_CFG0) #define MIPI_SHIM_BIAS_ENABLE 1 // Offset output pixels [1] #define MIPI_SHIM_STUFF_ENABLE 0 // Add a zero byte after every RGB pixel [2] @@ -64,8 +61,6 @@ void camera_main( #endif //__XC__ - - /* Notes [1] diff --git a/camera/api/camera_utils.h b/camera/api/camera_utils.h index 7b75deb4..dcb39705 100644 --- a/camera/api/camera_utils.h +++ b/camera/api/camera_utils.h @@ -21,8 +21,6 @@ extern "C" { */ unsigned measure_time(); - - /** * Convert an array of int8 to an array of uint8. * diff --git a/camera/api/image_hfilter.h b/camera/api/image_hfilter.h index 1f68b646..9c153e19 100644 --- a/camera/api/image_hfilter.h +++ b/camera/api/image_hfilter.h @@ -1,4 +1,3 @@ - #pragma once #include @@ -29,7 +28,6 @@ typedef struct { unsigned shift; } hfilter_state_t; - /** * This function performs a horizontal filtering operation on a single row * of pixels. @@ -56,7 +54,6 @@ void pixel_hfilter( const int32_t input_stride, const unsigned output_count); - /** * This function is used to update the filter parameters based on a gain to be * applied to the color channel represented by `state`. @@ -76,8 +73,6 @@ void pixel_hfilter_update_scale( const float gain, const unsigned offset); - - #if defined(__XC__) || defined(__cplusplus) } #endif diff --git a/camera/api/image_vfilter.h b/camera/api/image_vfilter.h index d47a2eb2..6181a17e 100644 --- a/camera/api/image_vfilter.h +++ b/camera/api/image_vfilter.h @@ -1,9 +1,8 @@ -#ifndef IMAGE_VFILTER_H -#define IMAGE_VFILTER_H +#pragma once #include -#include "sensor.h" +#include "sensor.h" // The number of non-zero taps in the vertical filter. #define VFILTER_TAP_COUNT (5) @@ -28,13 +27,10 @@ #define VFILTER_ACC_WIDTH_SHORTS (2*APP_IMAGE_WIDTH_PIXELS) - - #if defined(__XC__) || defined(__cplusplus) extern "C" { #endif - typedef struct { int next_tap; int16_t buff[VFILTER_ACC_WIDTH_SHORTS]; @@ -173,5 +169,3 @@ unsigned image_vfilter_drain( #if defined(__XC__) || defined(__cplusplus) } #endif - -#endif //IMAGE_VFILTER_H diff --git a/camera/api/isp.h b/camera/api/isp.h index 03dd6137..39885734 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -1,5 +1,4 @@ -#ifndef ISP_H -#define ISP_H +#pragma once #include // memset #include // null @@ -67,7 +66,6 @@ uint8_t AE_is_adjusted(float sk); */ uint8_t AE_compute_new_exposure(float exposure, float skewness); - // ---------------------------------- AWB ------------------------------ /** @@ -83,10 +81,9 @@ extern isp_params_t isp_params; /** * @brief auto white balance control function * - * @param gstats structure containing the global statistics * @param isp_params structure containing the current isp parameters */ -void AWB_compute_gains_static(global_stats_t *gstats, isp_params_t *isp_params); +void AWB_compute_gains_static(isp_params_t *isp_params); /** * @brief auto white balance control function based on white patch algorithm @@ -167,10 +164,9 @@ void isp_bilinear_resize( /** * Rotate the image by 90 degrees. This is useful for rotating images that are stored in a 3x3 array of uint8_t * -* @param filename - Name of the file to rotate * @param image - Array of uint8_t that is to be */ -void rotate_image_90(const char *filename, uint8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); +void rotate_image_90(uint8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]); // -------------------------- COLOR CONVERSION ------------------------------------- // Macro arguments to get color components from packed result in the assembly program @@ -207,5 +203,3 @@ int rgb_to_yuv( int r, int g, int b); - -#endif // ISP_H diff --git a/camera/api/packet_handler.h b/camera/api/packet_handler.h index 0a6d8df9..716eb8f9 100644 --- a/camera/api/packet_handler.h +++ b/camera/api/packet_handler.h @@ -1,6 +1,4 @@ - -#ifndef PACKET_HANDLER_H -#define PACKET_HANDLER_H +#pragma once #include @@ -22,16 +20,12 @@ typedef struct uint8_t payload[MIPI_MAX_PKT_SIZE_BYTES]; } mipi_packet_t; - - - /** * isp_params_t instance owned by the packet handler. */ extern isp_params_t isp_params; - /** * Thread entry point for packet handling when decimation and demosaicing are * desired. @@ -44,5 +38,3 @@ void mipi_packet_handler( #ifdef __XC__ } #endif - -#endif // PACKET_HANDLER_H diff --git a/camera/api/statistics.h b/camera/api/statistics.h index d51317a5..2ae465a8 100644 --- a/camera/api/statistics.h +++ b/camera/api/statistics.h @@ -1,5 +1,4 @@ - -#pragma once +#pragma once #include #include // memset @@ -17,7 +16,6 @@ extern "C" { #endif - #define CH_CARD (1<> (HIST_QUANT_BITS)) // Number of histogram bins #define APP_WB_PERCENTILE (0.94) @@ -49,7 +47,6 @@ typedef struct { typedef channel_stats_t global_stats_t[APP_IMAGE_CHANNEL_COUNT]; - // ---------- Api functions ---------- /** @@ -93,9 +90,6 @@ void stats_percentile_volume(channel_stats_t *stats); */ void stats_print(channel_stats_t *stats, unsigned channel); - - - #if defined(__XC__) || defined(__cplusplus) } #endif diff --git a/camera/src/camera_api.c b/camera/src/camera_api.c index 5a4c2ccc..973cebde 100644 --- a/camera/src/camera_api.c +++ b/camera/src/camera_api.c @@ -9,7 +9,6 @@ #include "camera_utils.h" #include "camera_api.h" - #define CHAN_RAW 0 #define CHAN_DEC 1 #define CHAN_STOP 2 @@ -17,7 +16,6 @@ // In order to interface the handler and api streaming_channel_t c_user_api[3]; - void camera_init() { c_user_api[CHAN_RAW] = s_chan_alloc(); @@ -89,8 +87,6 @@ unsigned camera_capture_row( return sdf; } - - unsigned camera_capture_row_decimated( int8_t pixel_data[CH][W]) { @@ -98,7 +94,6 @@ unsigned camera_capture_row_decimated( return s_chan_in_word(c_user_api[CHAN_DEC].end_b); } - unsigned camera_capture_image_raw( int8_t image_buff[H_RAW][W_RAW]) { @@ -149,8 +144,6 @@ unsigned camera_capture_image( return 0; } - - unsigned camera_capture_image_cropped( int8_t* image_buff, const image_crop_params_t crop_params) diff --git a/camera/src/camera_main.xc b/camera/src/camera_main.xc index 734a9e0f..74ee1572 100644 --- a/camera/src/camera_main.xc +++ b/camera/src/camera_main.xc @@ -1,4 +1,3 @@ - #include #include #include @@ -15,8 +14,6 @@ #include "isp.h" #include "sensor_control.h" - - void camera_main( tileref mipi_tile, in port p_mipi_clk, @@ -26,7 +23,6 @@ void camera_main( clock clk_mipi, client interface i2c_master_if i2c) { - streaming chan c_pkt; streaming chan c_ctrl; streaming chan c_stat_thread; diff --git a/camera/src/camera_utils.c b/camera/src/camera_utils.c index 3f21f029..3371b99e 100644 --- a/camera/src/camera_utils.c +++ b/camera/src/camera_utils.c @@ -1,4 +1,3 @@ - #include #include #include @@ -7,7 +6,6 @@ #include "camera_utils.h" - inline unsigned measure_time() { unsigned y = 0; @@ -19,8 +17,8 @@ inline unsigned measure_time() void vect_int8_to_uint8( uint8_t output[], int8_t input[], - const unsigned length) + const size_t length) { - for (int k = 0; k < length; k++) + for (size_t k = 0; k < length; k++) output[k] = input[k] + 128; } diff --git a/camera/src/image_hfilter.c b/camera/src/image_hfilter.c index bfba2f8a..c5d2ebe3 100644 --- a/camera/src/image_hfilter.c +++ b/camera/src/image_hfilter.c @@ -1,4 +1,3 @@ - #include #include #include @@ -6,10 +5,8 @@ #include "image_hfilter.h" #include "isp.h" - //Note: for filter coefficients reference : python/filters.txt - void pixel_hfilter_update_scale( hfilter_state_t* state, const float gain, diff --git a/camera/src/image_vfilter.c b/camera/src/image_vfilter.c index dba7a345..5cde939f 100644 --- a/camera/src/image_vfilter.c +++ b/camera/src/image_vfilter.c @@ -1,4 +1,3 @@ - #include #include #include @@ -33,7 +32,6 @@ void image_vfilter_reset( APP_IMAGE_WIDTH_PIXELS); } - /** * Call this once at the start of each frame. The accumulator next_tap values * are set somewhat differently than image_vfilter_reset(), because the behavior @@ -98,7 +96,6 @@ unsigned image_vfilter_process_row( return 0; } - unsigned image_vfilter_drain( int8_t output[], vfilter_acc_t acc[]) diff --git a/camera/src/isp_functions.c b/camera/src/isp_functions.c index 78760f67..09ff675f 100644 --- a/camera/src/isp_functions.c +++ b/camera/src/isp_functions.c @@ -157,7 +157,7 @@ void AWB_compute_gains_percentile(global_stats_t *gstats, isp_params_t *isp_para isp_params->channel_gain[2] = tmp2; } -void AWB_compute_gains_static(global_stats_t *gstats, isp_params_t *isp_params){ +void AWB_compute_gains_static(isp_params_t *isp_params){ // Adjust AWB float tmp0=1.4; float tmp1=1.1; @@ -168,7 +168,6 @@ void AWB_compute_gains_static(global_stats_t *gstats, isp_params_t *isp_params){ isp_params->channel_gain[2] = tmp2; } - void AWB_compute_gains_white_patch(global_stats_t *gstats, isp_params_t *isp_params){ float Rmax = (*gstats)[0].max; // RED float Gmax = (*gstats)[1].max; // GREEN @@ -187,7 +186,6 @@ void AWB_compute_gains_white_patch(global_stats_t *gstats, isp_params_t *isp_par isp_params->channel_gain[2] = gamma; } - void AWB_compute_gains_white_max(global_stats_t *gstats, isp_params_t *isp_params){ // we assume green constant const float beta = 1.0; @@ -223,8 +221,6 @@ void AWB_compute_gains_white_max(global_stats_t *gstats, isp_params_t *isp_param isp_params->channel_gain[2] = gamma; } - - void AWB_compute_gains_gray_world(global_stats_t *gstats, isp_params_t *isp_params){ printf("AWB --->"); float Ravg = (*gstats)[0].mean; // RED @@ -250,7 +246,6 @@ void AWB_print_gains(isp_params_t *isp_params){ isp_params->channel_gain[2]); } - // ---------------------------------- GAMMA ------------------------------ const uint8_t gamma_1p8_s1[256] = { @@ -303,14 +298,12 @@ const uint8_t gamma_new[256] = { 243, 244, 244, 245, 246, 246, 247, 248, 248, 249, 250, 250, 251, 252, 252, 253, 254, 254, 255, 255, 255, 255}; - void isp_gamma( uint8_t * img_in, const uint8_t *gamma_curve, const size_t height, const size_t width, - const size_t channels - ) + const size_t channels) { assert(gamma_curve[255] != 0); // ensure all values are filles up size_t buffsize = height * width * channels; @@ -375,9 +368,7 @@ void isp_bilinear_resize( } } -void rotate_image_90( - const char* filename, - uint8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]) +void rotate_image_90(uint8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]) { for(int c = 0; c < APP_IMAGE_CHANNEL_COUNT; c++) { for(int k = 0; k < APP_IMAGE_HEIGHT_PIXELS/2; k++) { diff --git a/camera/src/isp_pipeline.c b/camera/src/isp_pipeline.c index 521d2e1c..811b9f5e 100644 --- a/camera/src/isp_pipeline.c +++ b/camera/src/isp_pipeline.c @@ -64,6 +64,5 @@ void isp_pipeline(streaming_chanend_t c_img_in, CLIENT_INTERFACE(sensor_control_ // Print ISP info AWB_print_gains(&isp_params); - } } diff --git a/camera/src/packet_handler.c b/camera/src/packet_handler.c index a51f2c91..6d91b518 100644 --- a/camera/src/packet_handler.c +++ b/camera/src/packet_handler.c @@ -1,5 +1,3 @@ - - #include #include @@ -13,7 +11,6 @@ #include "camera_utils.h" #include "sensor.h" - // Filter stride #define HFILTER_INPUT_STRIDE (APP_DECIMATION_FACTOR) @@ -21,7 +18,6 @@ static vfilter_acc_t vfilter_accs[APP_IMAGE_CHANNEL_COUNT][VFILTER_ACC_COUNT]; - // Contains the local state info for the packet handler thread. static struct { unsigned wait_for_frame_start; @@ -38,8 +34,7 @@ static struct { hfilter_state_t hfilter_state[APP_IMAGE_CHANNEL_COUNT]; static -void handle_frame_start( - const mipi_packet_t* pkt) +void handle_frame_start() { // New frame is starting, reset the vertical filter accumulator states. for(int c = 0; c < APP_IMAGE_CHANNEL_COUNT; c++){ @@ -52,7 +47,6 @@ void handle_frame_start( } } - static void handle_unknown_packet( const mipi_packet_t* pkt) @@ -66,8 +60,6 @@ void handle_unknown_packet( assert(data_type < 0x3F && "Packet non valid"); } - - /** * Handle a row of pixel data. * @@ -146,7 +138,6 @@ unsigned handle_pixel_data( return 0; } - static void on_new_output_row( const int8_t pix_out[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_WIDTH_PIXELS], @@ -161,12 +152,9 @@ void on_new_output_row( ph_state.out_line_number++; } - - static void handle_frame_end( int8_t pix_out[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_WIDTH_PIXELS], - const mipi_packet_t* pkt, streaming_chanend_t c_stats) { // Drain the vertical filter's accumulators @@ -181,7 +169,6 @@ void handle_frame_end( s_chan_out_word(c_stats, (unsigned) NULL); } - void handle_no_expected_lines() { if(ph_state.in_line_number >= SENSOR_RAW_IMAGE_HEIGHT_PIXELS){ @@ -192,7 +179,6 @@ void handle_no_expected_lines() } } - /** * Process a single packet. * @@ -205,7 +191,6 @@ void handle_packet( const mipi_packet_t* pkt, streaming_chanend_t c_stats) { - /* * These buffers store rows of the fully decimated image. They are passed * along to the statistics thread once the packet handler thread no longer @@ -242,11 +227,11 @@ void handle_packet( ph_state.out_line_number = 0; ph_state.frame_number++; - handle_frame_start(pkt); + handle_frame_start(); break; case MIPI_DT_FRAME_END: - handle_frame_end(output_buff[out_dex], pkt, c_stats); + handle_frame_end(output_buff[out_dex], c_stats); out_dex = 1 - out_dex; break; @@ -319,6 +304,5 @@ void mipi_packet_handler( // unsigned time_start = measure_time(); handle_packet(pkt, c_stats); // unsigned time_proc = measure_time() - time_start; - } } diff --git a/camera/src/statistics.c b/camera/src/statistics.c index fca1bca4..5d3a3ac2 100644 --- a/camera/src/statistics.c +++ b/camera/src/statistics.c @@ -11,7 +11,6 @@ // This is the normalization factor static const float histogram_norm_factor = (1.0 / (float) HISTOGRAM_TOTAL_SAMPLES); - /** * Update histogram based on pixel values. * @@ -30,7 +29,6 @@ void stats_update_histogram( } } - void stats_skewness(channel_stats_t *stats) { const float zk_values[] = { @@ -55,7 +53,6 @@ void stats_skewness(channel_stats_t *stats) stats -> skewness = skew * histogram_norm_factor; } - void stats_simple(channel_stats_t *stats) { // Calculate the histogram @@ -85,7 +82,6 @@ void stats_simple(channel_stats_t *stats) stats->mean = (temp_mean) *(1 << HIST_QUANT_BITS) * histogram_norm_factor; } - void stats_print(channel_stats_t *stats, unsigned channel){ const char* formattedString = "\nch:%d,Mi:%d,Ma:%d,Mean:%f,Sk:%f,pct:%d,mi_c:%lu,ma_c:%lu,pc:%lu\n"; char output[255]; // Assuming a maximum length for the formatted string @@ -104,7 +100,6 @@ void stats_print(channel_stats_t *stats, unsigned channel){ // printf("%s", output); } - void stats_percentile(channel_stats_t *stats, const float fraction) { const unsigned threshold = fraction * HISTOGRAM_TOTAL_SAMPLES; diff --git a/examples/take_picture_downsample/CMakeLists.txt b/examples/take_picture_downsample/CMakeLists.txt index 112729b5..14f1f1de 100644 --- a/examples/take_picture_downsample/CMakeLists.txt +++ b/examples/take_picture_downsample/CMakeLists.txt @@ -12,42 +12,29 @@ set(APP_COMPILER_FLAGS -Os -g -report + -Wall + -Werror -fxscope -mcmodel=large - -Wno-xcore-fptrgroup -target=${XCORE_TARGET} ${CONFIG_XSCOPE_PATH}/config.xscope ) - -set(APP_COMPILE_DEFINITIONS - configENABLE_DEBUG_PRINTF=1 - PLATFORM_SUPPORTS_TILE_0=1 - PLATFORM_SUPPORTS_TILE_1=1 - PLATFORM_SUPPORTS_TILE_2=0 - PLATFORM_SUPPORTS_TILE_3=0 - PLATFORM_USES_TILE_0=1 - PLATFORM_USES_TILE_1=1 - XUD_CORE_CLOCK=600 -) - set(APP_LINK_OPTIONS "-report" "-target=${XCORE_TARGET}" "${CONFIG_XSCOPE_PATH}/config.xscope" ) - # <--- Link libraries set(APP_COMMON_LINK_LIBRARIES - mipi::lib_mipi - i2c::lib_i2c - sensors::lib_imx - camera::lib_camera - fwk_camera::utils - fwk_camera::xscope_fileio - ) - + mipi::lib_mipi + i2c::lib_i2c + sensors::lib_imx + camera::lib_camera + fwk_camera::utils + fwk_camera::xscope_fileio +) #********************** # Tile Targets @@ -58,8 +45,8 @@ target_sources(${TARGET} src/app.c src/main.xc ) + target_include_directories(${TARGET} PUBLIC src) -target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index d0715ea4..360d194d 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -1,4 +1,3 @@ - #include #include #include @@ -9,7 +8,6 @@ void user_app() { - // Initialize camera api camera_init(); diff --git a/examples/take_picture_raw/CMakeLists.txt b/examples/take_picture_raw/CMakeLists.txt index db596546..158e5f04 100644 --- a/examples/take_picture_raw/CMakeLists.txt +++ b/examples/take_picture_raw/CMakeLists.txt @@ -12,43 +12,29 @@ set(APP_COMPILER_FLAGS -Os -g -report + -Wall + -Werror -fxscope -mcmodel=large - -Wno-xcore-fptrgroup -target=${XCORE_TARGET} ${CONFIG_XSCOPE_PATH}/config.xscope ) - -set(APP_COMPILE_DEFINITIONS - configENABLE_DEBUG_PRINTF=1 - PLATFORM_SUPPORTS_TILE_0=1 - PLATFORM_SUPPORTS_TILE_1=1 - PLATFORM_SUPPORTS_TILE_2=0 - PLATFORM_SUPPORTS_TILE_3=0 - PLATFORM_USES_TILE_0=1 - PLATFORM_USES_TILE_1=1 - XUD_CORE_CLOCK=600 -) - set(APP_LINK_OPTIONS "-report" "-target=${XCORE_TARGET}" "${CONFIG_XSCOPE_PATH}/config.xscope" ) - # <--- Link libraries set(APP_COMMON_LINK_LIBRARIES - mipi::lib_mipi - i2c::lib_i2c - sensors::lib_imx - camera::lib_camera - fwk_camera::utils - fwk_camera::xscope_fileio - ) - - + mipi::lib_mipi + i2c::lib_i2c + sensors::lib_imx + camera::lib_camera + fwk_camera::utils + fwk_camera::xscope_fileio +) #********************** # Tile Targets @@ -59,8 +45,8 @@ target_sources(${TARGET} src/app_raw.c src/main.xc ) + target_include_directories(${TARGET} PUBLIC src) -target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) diff --git a/modules/i2c.cmake b/modules/i2c.cmake index a9a2c1fc..88bf48d0 100644 --- a/modules/i2c.cmake +++ b/modules/i2c.cmake @@ -1,32 +1,28 @@ - set(LIB_NAME lib_i2c) set(LIB_I2C_PATH ${CMAKE_CURRENT_LIST_DIR}/i2c/lib_i2c/) add_library(${LIB_NAME} STATIC) target_sources(${LIB_NAME} - PRIVATE - ${LIB_I2C_PATH}/src/i2c_master_async.xc - ${LIB_I2C_PATH}/src/i2c_master_ext.xc - ${LIB_I2C_PATH}/src/i2c_master_single_port.xc - ${LIB_I2C_PATH}/src/i2c_master.xc - ${LIB_I2C_PATH}/src/i2c_slave.xc + PRIVATE + ${LIB_I2C_PATH}/src/i2c_master_async.xc + ${LIB_I2C_PATH}/src/i2c_master_ext.xc + ${LIB_I2C_PATH}/src/i2c_master_single_port.xc + ${LIB_I2C_PATH}/src/i2c_master.xc + ${LIB_I2C_PATH}/src/i2c_slave.xc ) target_include_directories(${LIB_NAME} - PUBLIC - ${LIB_I2C_PATH}/api + PUBLIC + ${LIB_I2C_PATH}/api ) target_compile_options(${LIB_NAME} PRIVATE - -Os -g - -Wno-format - -Wno-unused-variable - -Wno-missing-braces + -Os -g -Wall -Werror ) -target_link_libraries( ${LIB_NAME} +target_link_libraries(${LIB_NAME} lib_xassert ) diff --git a/modules/mipi/CMakeLists.txt b/modules/mipi/CMakeLists.txt index 56a9624e..8b648249 100644 --- a/modules/mipi/CMakeLists.txt +++ b/modules/mipi/CMakeLists.txt @@ -1,13 +1,12 @@ - ## Target name set( LIB_NAME lib_mipi ) add_library(${LIB_NAME} STATIC) target_sources(${LIB_NAME} - PRIVATE - src/MipiPacketRx.S - src/MipiPacketRx.xc + PRIVATE + src/MipiPacketRx.S + src/MipiPacketRx.xc ) target_include_directories(${LIB_NAME} @@ -17,7 +16,7 @@ target_include_directories(${LIB_NAME} target_compile_options(${LIB_NAME} PRIVATE - -Os -g -Wall + -Os -g -Wall -Werror ) add_library(mipi::lib_mipi ALIAS ${LIB_NAME}) diff --git a/modules/xassert.cmake b/modules/xassert.cmake index 1a25e058..0e0e9a01 100644 --- a/modules/xassert.cmake +++ b/modules/xassert.cmake @@ -1,4 +1,3 @@ - set(LIB_NAME lib_xassert) set(LIB_XASSERT_PATH ${CMAKE_CURRENT_LIST_DIR}/xassert/lib_xassert) @@ -15,9 +14,6 @@ target_include_directories(${LIB_NAME} ) target_compile_options(${LIB_NAME} - PRIVATE - -Os -g - -Wno-format - -Wno-unused-variable - -Wno-missing-braces + PRIVATE + -Os -g -Wall -Werror ) diff --git a/sensors/CMakeLists.txt b/sensors/CMakeLists.txt index f3daa8bc..3a77bb80 100644 --- a/sensors/CMakeLists.txt +++ b/sensors/CMakeLists.txt @@ -33,6 +33,11 @@ target_sources(${LIB_NAME} ${SOURCES_ASM} ) +target_compile_options(${LIB_NAME} + PUBLIC + -Wall -Werror +) + target_link_libraries(${LIB_NAME} PUBLIC lib_i2c) add_library(sensors::lib_imx ALIAS ${LIB_NAME}) diff --git a/sensors/_galaxycore_gc2145/gc2145.xc b/sensors/_galaxycore_gc2145/gc2145.xc index 1103923c..305df43e 100644 --- a/sensors/_galaxycore_gc2145/gc2145.xc +++ b/sensors/_galaxycore_gc2145/gc2145.xc @@ -1,5 +1,6 @@ #include #include + #include #include "i2c.h" #include "gc2145.h" @@ -83,7 +84,6 @@ uint8_t i2c_read( void gc2145_print_info( client interface i2c_master_if i2c) { - i2c_regop_res_t result; uint8_t value; unsigned rewq; diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index 226fd5c1..23a15597 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -1,6 +1,5 @@ // Sensor.h settings needed for custom sensor configuration -#ifndef SENSOR_H -#define SENSOR_H +#pragma once #define XSTR(x) STR(x) #define STR(x) #x @@ -88,7 +87,6 @@ #endif #endif - // Cropping configurations #if (CROP_ENABLED) #define CROP_WIDTH_PIXELS 320 @@ -106,7 +104,6 @@ #endif - // ----------------------- Settings dependant of each sensor library // Camera dependant (do not edit) @@ -123,9 +120,6 @@ #warning "The image appears to be too large for the available internal RAM.!" #endif - - - // ---------------------------------------------------------------- // ---------------------------------------------------------------- @@ -149,7 +143,6 @@ #define APP_IMAGE_SIZE_BYTES (APP_IMAGE_SIZE_PIXELS \ * APP_IMAGE_CHANNEL_COUNT ) - #define CHAN_RED 0 #define CHAN_GREEN 1 #define CHAN_BLUE 2 @@ -160,7 +153,6 @@ #define HIST_QUANT_BITS (2) #endif - // Not every pixel of the image will be sampled. This is the distance between // sampled values in a row. #ifndef APP_HISTOGRAM_SAMPLE_STEP @@ -174,5 +166,3 @@ #define H_RAW (MIPI_IMAGE_HEIGHT_PIXELS) #define W_RAW (MIPI_IMAGE_WIDTH_PIXELS) - -#endif // sensor_H diff --git a/sensors/api/sensor_control.h b/sensors/api/sensor_control.h index 7d7dcbe4..f8f47fce 100644 --- a/sensors/api/sensor_control.h +++ b/sensors/api/sensor_control.h @@ -3,7 +3,6 @@ #include "xccompat.h" - #ifdef __XC__ typedef interface sensor_control_if { diff --git a/sensors/src/sensor_control.xc b/sensors/src/sensor_control.xc index bd23ac93..5ff58b22 100644 --- a/sensors/src/sensor_control.xc +++ b/sensors/src/sensor_control.xc @@ -18,7 +18,6 @@ void sensor_start(client interface i2c_master_if i2c){ assert((r == 0)); // assert that camera is started and configured } - void sensor_control( server interface sensor_control_if sc, client interface i2c_master_if i2c) @@ -37,7 +36,6 @@ void sensor_control( } } - void sensor_control_set_exposure( client interface sensor_control_if sc, unsigned exposure) diff --git a/tests/hardware_tests/test_timing/CMakeLists.txt b/tests/hardware_tests/test_timing/CMakeLists.txt index 1c6a638b..e9c4b5af 100644 --- a/tests/hardware_tests/test_timing/CMakeLists.txt +++ b/tests/hardware_tests/test_timing/CMakeLists.txt @@ -11,6 +11,8 @@ set(APP_COMPILER_FLAGS -Os -g -report + -Wall + -Werror -fxscope -target=${XCORE_TARGET} ${CONFIG_XSCOPE_PATH_TEST}/config.xscope @@ -23,10 +25,10 @@ set(APP_LINK_OPTIONS ) set(APP_COMMON_LINK_LIBRARIES - mipi::lib_mipi - i2c::lib_i2c - sensors::lib_imx - camera::lib_camera + mipi::lib_mipi + i2c::lib_i2c + sensors::lib_imx + camera::lib_camera ) # ############################################################################## diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index b53cbc07..aae3aa69 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -8,7 +8,7 @@ file(GLOB_RECURSE APP_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/*.*c) set(APP_COMMON_LINK_LIBRARIES Unity camera::lib_camera - ) +) # ############################################################################## # Flags @@ -24,17 +24,13 @@ set(APP_COMPILER_FLAGS -Os -g -report + -Wall + -Werror -fxscope -mcmodel=large - -Wno-xcore-fptrgroup ${CMAKE_CURRENT_LIST_DIR}/config.xscope ) -set(APP_COMPILE_DEFINITIONS - configENABLE_DEBUG_PRINTF=1 - XUD_CORE_CLOCK=600) - - set(APP_INCLUDES api) # ############################################################################## @@ -43,11 +39,9 @@ set(APP_INCLUDES api) add_executable(${TARGET} ) target_sources(${TARGET} PUBLIC ${APP_SOURCES}) target_include_directories(${TARGET} PUBLIC ${APP_INCLUDES}) -target_compile_definitions(${TARGET} PRIVATE ${APP_COMPILE_DEFINITIONS}) target_compile_options(${TARGET} PUBLIC -g -Os -Wall -Wno-xcore-fptrgroup) target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) - # enable testing functionality add_test(NAME ${TARGET} COMMAND ${TARGET}) diff --git a/tests/unity.cmake b/tests/unity.cmake index 906c87ea..a953c794 100644 --- a/tests/unity.cmake +++ b/tests/unity.cmake @@ -5,18 +5,24 @@ set(LIB_PATH ${CMAKE_CURRENT_LIST_DIR}/Unity) add_library(${LIB_NAME} STATIC) -target_include_directories( - ${LIB_NAME} PUBLIC - ${LIB_PATH}/src - ${LIB_PATH}/extras/fixture/src - ${LIB_PATH}/extras/memory/src +target_include_directories(${LIB_NAME} + PUBLIC + ${LIB_PATH}/src + ${LIB_PATH}/extras/fixture/src + ${LIB_PATH}/extras/memory/src ) target_sources(${LIB_NAME} - PUBLIC - ${LIB_PATH}/src/unity.c - ${LIB_PATH}/extras/fixture/src/unity_fixture.c - ${LIB_PATH}/extras/memory/src/unity_memory.c + PUBLIC + ${LIB_PATH}/src/unity.c + ${LIB_PATH}/extras/fixture/src/unity_fixture.c + ${LIB_PATH}/extras/memory/src/unity_memory.c +) + +# Suppressing weird warnings coming from unity +target_compile_options(${LIB_NAME} + PUBLIC + -Wno-xcore-fptrgroup ) add_library(Unity::framework ALIAS ${LIB_NAME}) diff --git a/utils/io_utils/io_utils.c b/utils/io_utils/io_utils.c index 6bd4915a..0b86445e 100644 --- a/utils/io_utils/io_utils.c +++ b/utils/io_utils/io_utils.c @@ -1,4 +1,3 @@ - #include "io_utils.h" #include From c472396ab6168b6fc44f174786842c695f51f707 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 27 Jun 2023 15:20:11 +0100 Subject: [PATCH 149/306] updating readmes --- README.rst | 4 +--- examples/take_picture_downsample/README.rst | 15 ++++++++------- examples/take_picture_raw/README.rst | 16 ++++++++-------- requirements.txt | 4 +++- tests/readme.rst | 6 +++--- 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/README.rst b/README.rst index 2462d28c..a3364baa 100644 --- a/README.rst +++ b/README.rst @@ -46,7 +46,7 @@ Linux, Mac make Windows -~~~~~~~~ +~~~~~~~ .. code-block:: bash @@ -54,8 +54,6 @@ Windows cd build/ ninja - - Supported Cameras ----------------- diff --git a/examples/take_picture_downsample/README.rst b/examples/take_picture_downsample/README.rst index a3c3a9db..e2cde567 100644 --- a/examples/take_picture_downsample/README.rst +++ b/examples/take_picture_downsample/README.rst @@ -15,14 +15,14 @@ Run the following commands from the top level: .. code-block:: console - cmake -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -B build + cmake -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake make -C build example_take_picture_downsample .. tab:: Windows .. code-block:: console - cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -B build + cmake -G "Ninja" -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake ninja -C build example_take_picture_downsample *************** @@ -36,18 +36,19 @@ From the top level .. code-block:: console pip install -e utils/xscope_fileio - python python/run_xscope_bin.py build/examples/take_picture_downsample/example_take_picture_downsample.xe + cd python + python run_xscope_bin.py ../build/examples/take_picture_downsample/example_take_picture_downsample.xe .. tab:: Windows .. code-block:: console # works with a cl compiler - pip install -e utils/xscope_fileio - cd utils/xscope_fileio/host + pip install -e utils\xscope_fileio + cd utils\xscope_fileio\host cmake -G "Ninja" . && ninja - cd ../../../ - python python/run_xscope_bin.py build/examples/take_picture_downsample/example_take_picture_downsample.xe + cd ..\..\..\python + python run_xscope_bin.py ..\build\examples\take_picture_downsample\example_take_picture_downsample.xe ****** Output diff --git a/examples/take_picture_raw/README.rst b/examples/take_picture_raw/README.rst index f69a3efd..c0c577a5 100644 --- a/examples/take_picture_raw/README.rst +++ b/examples/take_picture_raw/README.rst @@ -15,14 +15,14 @@ Run the following commands from the top level: .. code-block:: console - cmake -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -B build + cmake -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake make -C build example_take_picture_raw .. tab:: Windows .. code-block:: console - cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -B build + cmake -G "Ninja" -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake -B build ninja -C build example_take_picture_raw *************** @@ -36,19 +36,19 @@ From the top level .. code-block:: console pip install -e utils/xscope_fileio - python python/run_xscope_bin.py build/examples/take_picture_raw/example_take_picture_raw.xe + cd python + python run_xscope_bin.py ../build/examples/take_picture_raw/example_take_picture_raw.xe .. tab:: Windows .. code-block:: console # works with a cl compiler - pip install -e utils/xscope_fileio - cd utils/xscope_fileio/host + pip install -e utils\xscope_fileio + cd utils\xscope_fileio\host cmake -G "Ninja" . && ninja - cd ../../../ - python python/run_xscope_bin.py build/examples/take_picture_raw/example_take_picture_raw.xe - + cd ..\..\..\python + python run_xscope_bin.py ..\build\examples\take_picture_raw\example_take_picture_raw.xe ****** Output diff --git a/requirements.txt b/requirements.txt index bb265a70..ed4704b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,6 +16,8 @@ python-dateutil==2.8.2 python-dotenv==1.0.0 PyWavelets==1.4.1 scikit-image==0.20.0 -scipy==1.10.1 +scipy six==1.16.0 tifffile==2023.4.12 + +-e ./utils/xscope_fileio diff --git a/tests/readme.rst b/tests/readme.rst index d37f1eb4..36e6ced8 100644 --- a/tests/readme.rst +++ b/tests/readme.rst @@ -15,12 +15,12 @@ Run the following commands from the top level: .. code-block:: console - cmake -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -B build + cmake -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake make -C build tests .. code-block:: console - cmake -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake -B build + cmake -G "Ninja" -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake -B build ninja -C build tests @@ -37,7 +37,7 @@ Run unit tests .. code-block:: bash xsim --xscope "-offline trace.xmt" build/tests/unit_tests/test_camera.xe - or + # or xrun --xscope build/tests/unit_tests/test_camera.xe Run hardware tests From d2b905002fe0230b97fbc5ca2127f4295608fe45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Tue, 27 Jun 2023 15:23:45 +0100 Subject: [PATCH 150/306] architecture and desing --- doc/1_programming_guide/01_Introduction.rst | 28 +++++----------- .../02_Architecture_and_Design.rst | 31 +++++++++++++----- .../images/2_functional_object.png | Bin 258737 -> 0 bytes .../images/2_object_diagram.png | Bin 0 -> 182746 bytes .../images/2_object_diagram.svg | 4 +++ doc/2_quick_start_guide/.gitkeep | 0 doc/2_quick_start_guide/Quick_Start_guide.rst | 6 ++-- doc/substitutions.rst | 3 +- 8 files changed, 40 insertions(+), 32 deletions(-) delete mode 100644 doc/1_programming_guide/images/2_functional_object.png create mode 100644 doc/1_programming_guide/images/2_object_diagram.png create mode 100644 doc/1_programming_guide/images/2_object_diagram.svg delete mode 100644 doc/2_quick_start_guide/.gitkeep diff --git a/doc/1_programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst index 7be995de..2efac3ac 100644 --- a/doc/1_programming_guide/01_Introduction.rst +++ b/doc/1_programming_guide/01_Introduction.rst @@ -5,7 +5,8 @@ Introduction Overview --------- -The purpose of this programming guide is to provide developers with a comprehensive understanding of the FWK_Camera architecture and guide them on how to effectively interact with cameras using the |XCORE-AI-EXPLORER| board. +The purpose of this programming guide is to provide developers with a comprehensive understanding +of the FWK_Camera architecture and guide them on how to effectively interact with cameras using the XCORE-AI-EXPLORER board. Conventions and Terminology --------------------------- @@ -26,10 +27,10 @@ Features - GC2145 (explain hardware modification) -The |XCORE-AI-EXPLORER| board features an 15-pin MIPI CSI2 port (compatible with Raspberry PI). -This port is connected to the |Xcore-AI| processor, so we can directly processing an image from an external sensor and performing various operations, +The XCORE-AI-EXPLORER board features an 15-pin MIPI CSI2 port (compatible with Raspberry PI). +This port is connected to the Xcore-AI processor, so we can directly processing an image from an external sensor and performing various operations, such as converting a RAW image to an RGB image (applying ISP functions), -analyzing the image using AI models with |xmos-ai-tools|, +analyzing the image using AI models with xmos-ai-tools, converting a MIPI camera to other interfaces as USB, SPI, etc. This repository contains a set of tools for image acquisition, processing, and transmission. @@ -52,22 +53,9 @@ Each of these elements is described in detail in the following sections. Getting Started ---------------- -To start using the FWK_Camera, you can proceed to the Quick Start Guide: `QS_FWKC`_. - - -Hardware requirements: - -- XCORE.AI EVALUATION KIT (XK-EVK-XU316) -- Camera module -- Power supply -- Micro USB cable -- JTAG debugger - -Software requirements: - -- XMOS tools: `SW_TOOLS`_ -- FWK_Camera repository: `GH_FWK_CAMERA`_ -- CMake, Ninja (Windows) +To start using the FWK_Camera, you can proceed to the Quick Start Guide, go to: + + :ref:`QS_FWKC`. Additional Resources --------------------- diff --git a/doc/1_programming_guide/02_Architecture_and_Design.rst b/doc/1_programming_guide/02_Architecture_and_Design.rst index 9d1f8e4d..76812766 100644 --- a/doc/1_programming_guide/02_Architecture_and_Design.rst +++ b/doc/1_programming_guide/02_Architecture_and_Design.rst @@ -6,23 +6,36 @@ Architecture and Design Introduction ------------- -System Architecture --------------------- +In this section we describe the main components of fwk_camera and how they interact with each other. We also describe the design decisions that were made and the reasoning behind them. The main components of fwk_camera are the following: -.. figure:: images/2_functional_object.png + +Hardware Architecture +---------------------- +From a Hardware point of view, fwk_camera is composed of the following modules: +1. + + +Software Architecture +------------------------- +From a Software point of view, fwk_camera is composed of the following modules: + +.. figure:: images/2_object_diagram.svg :alt: Alternative Text - :figwidth: 400px :figclass: custom-class - Class Diagram + Object Diagram +Module description: + 1. camera section: the camera section takes care of start, process and stop the camera. + It has to be aware of the sensor configuration (in sesnor section) and meet the user demands (in user section). + 2. user section: the user section is the interface between the user and the camera, where we define what we want to do with the frames. + 3. sensor section: configuration and control of the sensor. + 4. (host) section: provide the interface to write the frames or files to the host. -Module Descriptions ---------------------- +There are other sections not mentioned in the diagram, as the test section, which is used to test the camera. Optimizations and Future Directions ------------------------------------ -Conclusion ------------- +|TBD| diff --git a/doc/1_programming_guide/images/2_functional_object.png b/doc/1_programming_guide/images/2_functional_object.png deleted file mode 100644 index e5eab3fe6efb9dcce10586ae1f61b07609215fa4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 258737 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 q4lUKa;^>q) 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

a7`;&n%CtlN$=M8K5Zb(AcUM;u| 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 literal 0 HcmV?d00001 diff --git a/doc/1_programming_guide/images/2_object_diagram.svg b/doc/1_programming_guide/images/2_object_diagram.svg new file mode 100644 index 00000000..81c43ca1 --- /dev/null +++ b/doc/1_programming_guide/images/2_object_diagram.svg @@ -0,0 +1,4 @@ + + + +
camera_main
camera_main
 mipi_packet_
handler
mipi_packet_...
statistics_thread
statistics_thread
MipiPacketRx
MipiPacketRx
isp
(image signal processing)
isp...
image_filtering
image_filtering
user
app
user...
user
main
user...
i2c
i2c
xscope
file_io
xscope...
External Host
External Host
RAW
IMAGE
RAW...
DOWNSAMPLED IMAGE
DOWNSAMPLED IMAGE
camera
camera
camera_api
camera_api
user
user
host
host
sensor
sensor
camera 1
IMX219
camera 1...
camera 2
GC2145
camera 2...
sensor
api
sensor...
sensor_control
sensor_control
camera_init
camera_init
camera_start
camera_start
camera_stop
camera_stop
sensor
sensor
camera 1
IMX219
camera 1...
camera 2
GC2145
camera 2...
sensor
api
sensor...
sensor_control
sensor_control
sensor_init
sensor_init
sensor_configure
sensor_configure
sensor_start
sensor_start
sensor_stop
sensor_stop
Sensor
....
Sensor...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/doc/2_quick_start_guide/.gitkeep b/doc/2_quick_start_guide/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/doc/2_quick_start_guide/Quick_Start_guide.rst b/doc/2_quick_start_guide/Quick_Start_guide.rst index ed58da6f..7faefe12 100644 --- a/doc/2_quick_start_guide/Quick_Start_guide.rst +++ b/doc/2_quick_start_guide/Quick_Start_guide.rst @@ -1,5 +1,7 @@ -Getting Started ----------------- +.. _QS_FWKC: + +Quick Start Guide +------------------- Hardware requirements: ^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/substitutions.rst b/doc/substitutions.rst index 03c99287..26f225df 100644 --- a/doc/substitutions.rst +++ b/doc/substitutions.rst @@ -1,3 +1,4 @@ + .. _MIPI: https://www.mipi.org/specifications/csi-2 .. _XMOS: https://www.xmos.ai/ .. _XMOSI2C: https://www.xmos.ai/download/lib_i2c-%5Buserguide%5D(5.0.0rc3).pdf @@ -7,4 +8,4 @@ .. _SW_TOOLS: https://www.xmos.ai/software-tools/ -.. EVK_BOARD: XCORE-AI-EXPLORER board +.. |TBD| replace:: ``To be documented for next release`` From ef56c57c17f1c29541f2f24483d9b6a072119c41 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 27 Jun 2023 16:08:21 +0100 Subject: [PATCH 151/306] common compiler flags for examples, updating readmes --- README.rst | 14 +++--- examples/CMakeLists.txt | 28 ++++++++++++ .../take_picture_downsample/CMakeLists.txt | 45 +++---------------- examples/take_picture_raw/CMakeLists.txt | 45 +++---------------- tests/readme.rst | 10 +++-- 5 files changed, 52 insertions(+), 90 deletions(-) diff --git a/README.rst b/README.rst index a3364baa..ea0ea889 100644 --- a/README.rst +++ b/README.rst @@ -7,9 +7,9 @@ Repository Structure -------------------- - **examples** : examples for taking pictures with the explorer board -- **lib_camera** : useful functions to manipulate images +- **camera** : useful functions to manipulate images - **modules** : dependencies folder -- **sensors** : camera sensors and API for controlling any camera sensor +- **sensors** : configuration and control of the sensors - **python** : python functions to decode RAW8, RAW10 pictures and other utilities to treat images Requirements @@ -39,7 +39,7 @@ For building a specific example refer to examples/readme.rst. Linux, Mac ~~~~~~~~~~ -.. code-block:: bash +.. code-block:: console cmake -B build -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake cd build/ @@ -48,10 +48,10 @@ Linux, Mac Windows ~~~~~~~ -.. code-block:: bash +.. code-block:: console - cmake -G Ninja -B build -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake - cd build/ + cmake -G Ninja -B build -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain\xs3a.cmake + cd build ninja Supported Cameras @@ -68,6 +68,6 @@ Supported Cameras +--------+--------------------------------+----------------+ How to configure your sensor or add a new one --------------------------------------------- +--------------------------------------------- TODO diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index a8c16350..cdf3408b 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,6 +1,34 @@ # set xscope config file path set(CONFIG_XSCOPE_PATH ${CMAKE_CURRENT_SOURCE_DIR}) +# set compiler/linker flags and liked libraries to be used by all examples +set(EXAMPLE_APP_COMPILER_FLAGS + -Os + -g + -report + -Wall + -Werror + -fxscope + -mcmodel=large + -target=${XCORE_TARGET} + ${CONFIG_XSCOPE_PATH}/config.xscope +) + +set(EXAMPLE_APP_LINK_OPTIONS + "-report" + "-target=${XCORE_TARGET}" + "${CONFIG_XSCOPE_PATH}/config.xscope" +) + +set(EXAMPLE_APP_LINK_LIBRARIES + mipi::lib_mipi + i2c::lib_i2c + sensors::lib_imx + camera::lib_camera + fwk_camera::utils + fwk_camera::xscope_fileio +) + # add the examples add_subdirectory(take_picture_raw) add_subdirectory(take_picture_downsample) diff --git a/examples/take_picture_downsample/CMakeLists.txt b/examples/take_picture_downsample/CMakeLists.txt index 14f1f1de..cd566588 100644 --- a/examples/take_picture_downsample/CMakeLists.txt +++ b/examples/take_picture_downsample/CMakeLists.txt @@ -1,43 +1,8 @@ -#********************** -# Gather Sources -#********************** - -# <--- Set the executable +# <--- Set the executable name set(TARGET example_take_picture_downsample) #********************** -# Flags -#********************** -set(APP_COMPILER_FLAGS - -Os - -g - -report - -Wall - -Werror - -fxscope - -mcmodel=large - -target=${XCORE_TARGET} - ${CONFIG_XSCOPE_PATH}/config.xscope -) - -set(APP_LINK_OPTIONS - "-report" - "-target=${XCORE_TARGET}" - "${CONFIG_XSCOPE_PATH}/config.xscope" -) - -# <--- Link libraries -set(APP_COMMON_LINK_LIBRARIES - mipi::lib_mipi - i2c::lib_i2c - sensors::lib_imx - camera::lib_camera - fwk_camera::utils - fwk_camera::xscope_fileio -) - -#********************** -# Tile Targets +# Targets #********************** add_executable(${TARGET}) target_sources(${TARGET} @@ -47,6 +12,6 @@ target_sources(${TARGET} ) target_include_directories(${TARGET} PUBLIC src) -target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) -target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) -target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) +target_compile_options(${TARGET} PRIVATE ${EXAMPLE_APP_COMPILER_FLAGS}) +target_link_libraries(${TARGET} PUBLIC ${EXAMPLE_APP_LINK_LIBRARIES}) +target_link_options(${TARGET} PRIVATE ${EXAMPLE_APP_LINK_OPTIONS}) diff --git a/examples/take_picture_raw/CMakeLists.txt b/examples/take_picture_raw/CMakeLists.txt index 158e5f04..9ea7118e 100644 --- a/examples/take_picture_raw/CMakeLists.txt +++ b/examples/take_picture_raw/CMakeLists.txt @@ -1,43 +1,8 @@ -#********************** -# Gather Sources -#********************** - -# <--- Set the executable +# <--- Set the executable name set(TARGET example_take_picture_raw) #********************** -# Flags -#********************** -set(APP_COMPILER_FLAGS - -Os - -g - -report - -Wall - -Werror - -fxscope - -mcmodel=large - -target=${XCORE_TARGET} - ${CONFIG_XSCOPE_PATH}/config.xscope -) - -set(APP_LINK_OPTIONS - "-report" - "-target=${XCORE_TARGET}" - "${CONFIG_XSCOPE_PATH}/config.xscope" -) - -# <--- Link libraries -set(APP_COMMON_LINK_LIBRARIES - mipi::lib_mipi - i2c::lib_i2c - sensors::lib_imx - camera::lib_camera - fwk_camera::utils - fwk_camera::xscope_fileio -) - -#********************** -# Tile Targets +# Targets #********************** add_executable(${TARGET}) target_sources(${TARGET} @@ -47,6 +12,6 @@ target_sources(${TARGET} ) target_include_directories(${TARGET} PUBLIC src) -target_compile_options(${TARGET} PRIVATE ${APP_COMPILER_FLAGS}) -target_link_libraries(${TARGET} PUBLIC ${APP_COMMON_LINK_LIBRARIES}) -target_link_options(${TARGET} PRIVATE ${APP_LINK_OPTIONS}) +target_compile_options(${TARGET} PRIVATE ${EXAMPLE_APP_COMPILER_FLAGS}) +target_link_libraries(${TARGET} PUBLIC ${EXAMPLE_APP_LINK_LIBRARIES}) +target_link_options(${TARGET} PRIVATE ${EXAMPLE_APP_LINK_OPTIONS}) diff --git a/tests/readme.rst b/tests/readme.rst index 36e6ced8..4eb998af 100644 --- a/tests/readme.rst +++ b/tests/readme.rst @@ -7,23 +7,27 @@ It contains two types of tests: 1. Unit tests 2. Hardware tests - Build Tests ============= Run the following commands from the top level: +Linux, Mac +~~~~~~~~~~ + .. code-block:: console cmake -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake make -C build tests +Windows +~~~~~~~ + .. code-block:: console - cmake -G "Ninja" -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake -B build + cmake -G "Ninja" -B build --toolchain=xmos_cmake_toolchain\xs3a.cmake ninja -C build tests - Running the tests ================= From 7ea1e3eeea5a16392bcc2759b9ecde2e1348dfe7 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 27 Jun 2023 16:11:37 +0100 Subject: [PATCH 152/306] readme formatting --- README.rst | 4 ++-- tests/{readme.rst => README.rst} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename tests/{readme.rst => README.rst} (98%) diff --git a/README.rst b/README.rst index ea0ea889..c0ea86e4 100644 --- a/README.rst +++ b/README.rst @@ -41,7 +41,7 @@ Linux, Mac .. code-block:: console - cmake -B build -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain/xs3a.cmake + cmake -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake cd build/ make @@ -50,7 +50,7 @@ Windows .. code-block:: console - cmake -G Ninja -B build -DCMAKE_TOOLCHAIN_FILE=xmos_cmake_toolchain\xs3a.cmake + cmake -G Ninja -B build --toolchain=xmos_cmake_toolchain\xs3a.cmake cd build ninja diff --git a/tests/readme.rst b/tests/README.rst similarity index 98% rename from tests/readme.rst rename to tests/README.rst index 4eb998af..b4e274f7 100644 --- a/tests/readme.rst +++ b/tests/README.rst @@ -13,7 +13,7 @@ Build Tests Run the following commands from the top level: Linux, Mac -~~~~~~~~~~ +---------- .. code-block:: console @@ -21,7 +21,7 @@ Linux, Mac make -C build tests Windows -~~~~~~~ +------- .. code-block:: console From 1bb3a062eb34c9f554466f421bae8cfa3f6ce601 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 27 Jun 2023 16:16:10 +0100 Subject: [PATCH 153/306] changing all bash to console in rst --- README.rst | 2 +- tests/README.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index c0ea86e4..4ad62467 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ Installation Some dependent components are included as git submodules. These can be obtained by cloning this repository with the following command: (make sure you have the correct ssh access to clone) -.. code-block:: bash +.. code-block:: console git clone --recurse-submodules git@github.com:xmos/fwk_camera.git diff --git a/tests/README.rst b/tests/README.rst index b4e274f7..5854b00c 100644 --- a/tests/README.rst +++ b/tests/README.rst @@ -38,7 +38,7 @@ Running the tests Run unit tests -------------- -.. code-block:: bash +.. code-block:: console xsim --xscope "-offline trace.xmt" build/tests/unit_tests/test_camera.xe # or @@ -47,6 +47,6 @@ Run unit tests Run hardware tests ------------------ -.. code-block:: bash +.. code-block:: console pytest From b994c4326abb3733df8297a9f54257ce65f60781 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 27 Jun 2023 16:18:10 +0100 Subject: [PATCH 154/306] typo --- examples/take_picture_raw/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/take_picture_raw/README.rst b/examples/take_picture_raw/README.rst index c0c577a5..f608fece 100644 --- a/examples/take_picture_raw/README.rst +++ b/examples/take_picture_raw/README.rst @@ -22,7 +22,7 @@ Run the following commands from the top level: .. code-block:: console - cmake -G "Ninja" -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake -B build + cmake -G "Ninja" -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake ninja -C build example_take_picture_raw *************** From 2de66e92fd399e175141c1640019c23d07d5205f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Tue, 27 Jun 2023 16:36:34 +0100 Subject: [PATCH 155/306] simple structure refactor --- doc/1_programming_guide/01_Introduction.rst | 20 +++++++----- .../02_Architecture_and_Design.rst | 20 ++++++------ .../03_Building_and_testing_the_Software.rst | 26 ++++++++++++++-- .../04_Developing_custom_software.rst | 8 +++-- .../05_FAQ_and_Troubleshooting.rst | 8 +++-- doc/1_programming_guide/index.rst | 2 ++ doc/2_quick_start_guide/Quick_Start_guide.rst | 31 +++++++++++++++---- doc/substitutions.rst | 3 +- 8 files changed, 86 insertions(+), 32 deletions(-) diff --git a/doc/1_programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst index 2efac3ac..dee0b334 100644 --- a/doc/1_programming_guide/01_Introduction.rst +++ b/doc/1_programming_guide/01_Introduction.rst @@ -19,20 +19,21 @@ To ensure clarity and consistency throughout this guide, the following conventio Features --------- -- MIPI CSI2 interface -- Up to 1GBps per lane -- Low-resolution filtering -- Supported cameras: - - IMX219 - - GC2145 (explain hardware modification) - - The XCORE-AI-EXPLORER board features an 15-pin MIPI CSI2 port (compatible with Raspberry PI). This port is connected to the Xcore-AI processor, so we can directly processing an image from an external sensor and performing various operations, such as converting a RAW image to an RGB image (applying ISP functions), analyzing the image using AI models with xmos-ai-tools, converting a MIPI camera to other interfaces as USB, SPI, etc. +The FWK_Camera alongside with the Explorer board architecture provides the following features: + +- MIPI CSI2 interface +- Up to 1GBps per lane +- Low-resolution filtering +- Supported cameras: + - IMX219 + - GC2145 [*]_ + This repository contains a set of tools for image acquisition, processing, and transmission. The architecture, viewed from a high level, is composed of the following elements: @@ -64,3 +65,6 @@ Additional Resources - XMOS I2C library user guide: `XMOSI2C`_ - XMOS Programming Guide: `XMOSProgrammingGuide`_ - IMX219 datasheet: `IMX219`_ + + +.. [*] With Hardware modifications. diff --git a/doc/1_programming_guide/02_Architecture_and_Design.rst b/doc/1_programming_guide/02_Architecture_and_Design.rst index 76812766..18e6ff0c 100644 --- a/doc/1_programming_guide/02_Architecture_and_Design.rst +++ b/doc/1_programming_guide/02_Architecture_and_Design.rst @@ -2,7 +2,7 @@ Architecture and Design ======================= .. include:: ../substitutions.rst - + Introduction ------------- @@ -12,8 +12,11 @@ In this section we describe the main components of fwk_camera and how they inter Hardware Architecture ---------------------- From a Hardware point of view, fwk_camera is composed of the following modules: -1. +#. Camera connector : 15-PIN MIPI CSI connector for the camera modul#. (compatible with Raspberry Pi). +#. Mipi Shim : Xmos MIPI hardware to convert MIPI signals to Xcore p#.ts. +#. Xcore : Xmos Xcore processor to process the MIPI signal#. +#. Jtag Host connector : USB connector to connect to the h#.t. Software Architecture ------------------------- @@ -26,16 +29,15 @@ From a Software point of view, fwk_camera is composed of the following modules: Object Diagram Module description: - 1. camera section: the camera section takes care of start, process and stop the camera. - It has to be aware of the sensor configuration (in sesnor section) and meet the user demands (in user section). - 2. user section: the user section is the interface between the user and the camera, where we define what we want to do with the frames. - 3. sensor section: configuration and control of the sensor. - 4. (host) section: provide the interface to write the frames or files to the host. +#. camera section: the camera section takes care of start, process and stop the camera. It has to be aware of the sensor configuration (in sesnor section) and meet the user demands (in user section). +#. user section: the user section is the interface between the user and the camera, where we define what we want to do with the frames. +#. sensor section: configuration and control of the sensor. +#. Host section: provide the interface to write the frames or files to the host. There are other sections not mentioned in the diagram, as the test section, which is used to test the camera. -Optimizations and Future Directions +Optimizations and future directions ------------------------------------ -|TBD| + |TBD| diff --git a/doc/1_programming_guide/03_Building_and_testing_the_Software.rst b/doc/1_programming_guide/03_Building_and_testing_the_Software.rst index c0c195df..08a4ea6a 100644 --- a/doc/1_programming_guide/03_Building_and_testing_the_Software.rst +++ b/doc/1_programming_guide/03_Building_and_testing_the_Software.rst @@ -1,21 +1,41 @@ Building the Software ======================= + +.. include:: ../substitutions.rst + This section will provide details on how the software is constructed. The basic steps and build requirements can be found in the README.md file which is distributed with the source. -.. include:: ../substitutions.rst Requirements ------------------------------------------ + |TBD| + + + Introduction ------------------------------------------ + |TBD| + + + +Building the host app (xscope_fileio) +------------------------------------------ + + |TBD| + + + Bulding the firmware and the examples ------------------------------------------ + |TBD| + + + Adding new files ------------------------------------------ -Building the host app (xscope_fileio) ------------------------------------------- + |TBD| diff --git a/doc/1_programming_guide/04_Developing_custom_software.rst b/doc/1_programming_guide/04_Developing_custom_software.rst index c21656ff..83e180c1 100644 --- a/doc/1_programming_guide/04_Developing_custom_software.rst +++ b/doc/1_programming_guide/04_Developing_custom_software.rst @@ -1,4 +1,6 @@ -Introduction -============= +Software customization +======================= -//TODO include how to ad da new camera +.. include:: ../substitutions.rst + +|TBD| diff --git a/doc/1_programming_guide/05_FAQ_and_Troubleshooting.rst b/doc/1_programming_guide/05_FAQ_and_Troubleshooting.rst index 250cc8bf..15de6d11 100644 --- a/doc/1_programming_guide/05_FAQ_and_Troubleshooting.rst +++ b/doc/1_programming_guide/05_FAQ_and_Troubleshooting.rst @@ -1,2 +1,6 @@ -Introduction -============= +FAQ and troubleshooting +========================= + +.. include:: ../substitutions.rst + +|TBD| diff --git a/doc/1_programming_guide/index.rst b/doc/1_programming_guide/index.rst index a8e4348c..0a4a2b6e 100644 --- a/doc/1_programming_guide/index.rst +++ b/doc/1_programming_guide/index.rst @@ -2,6 +2,8 @@ Programming Guide ******************* +.. include:: ../substitutions.rst + .. toctree:: :maxdepth: 1 diff --git a/doc/2_quick_start_guide/Quick_Start_guide.rst b/doc/2_quick_start_guide/Quick_Start_guide.rst index 7faefe12..cdd1933c 100644 --- a/doc/2_quick_start_guide/Quick_Start_guide.rst +++ b/doc/2_quick_start_guide/Quick_Start_guide.rst @@ -3,6 +3,8 @@ Quick Start Guide ------------------- +.. include:: ../substitutions.rst + Hardware requirements: ^^^^^^^^^^^^^^^^^^^^^^^ - XCORE.AI EVALUATION KIT (XK-EVK-XU316) @@ -14,10 +16,14 @@ Hardware requirements: Software requirements: ^^^^^^^^^^^^^^^^^^^^^^^ - XMOS tools: `SW_TOOLS`_ -- Xscope FIleio: `xscope_fileio`_ - CMake, Ninja (Windows) - Python 3.7 or later +(Make sure all submodules are imported) + +.. code-block:: console + + git clone --recurse-submodules git@github.com:xmos/fwk_camera.git Run the RAW camera demo ^^^^^^^^^^^^^^^^^^^^^^^ @@ -27,10 +33,23 @@ Then this image can be decoded using the `RAW image decoder`_. 1. Make sure that the camera is connecte to the board 2. Connect Power Supply and JTAG debugger 3. Build he example using the following commands: - a. `cmake -G "Ninja" -DTOOLCHAIN=xmos_toolchain -B build -S .` - b. `ninja -C build example_take_picture_raw` + +.. code-block:: console + + cmake -G Ninja -B build --toolchain=xmos_cmake_toolchain\xs3a.cmake + ninja -C build example_take_picture_raw + 4. Run the example using the following command: - a. `python python/run_xscope_bin.py build/examples/take_picture_raw/example_take_picture_raw.xe` + +.. code-block:: console + + python python/run_xscope_bin.py build/examples/take_picture_raw/example_take_picture_raw.xe + 5. You should see the camera comminucating with the host and the image being saved to a .raw file -6. To decode the image use the `RAW image decoder`_ and the following command: - a. `python decode_raw8.py` +6. To decode the image use the following command: + +.. code-block:: console + + python python/decode_raw8.py + +7. You should see the decoded image displayed on the screen diff --git a/doc/substitutions.rst b/doc/substitutions.rst index 26f225df..93d0f5ed 100644 --- a/doc/substitutions.rst +++ b/doc/substitutions.rst @@ -6,6 +6,7 @@ .. _IMX219: https://www.opensourceinstruments.com/Electronics/Data/IMX219PQ.pdf .. _GH_FWK_CAMERA: https://github.com/xmos/fwk_camera .. _SW_TOOLS: https://www.xmos.ai/software-tools/ +.. _XSCOPE_FILEIO: https://github.com/xmos/xscope_fileio -.. |TBD| replace:: ``To be documented for next release`` +.. |TBD| replace:: To be documented for next release From 6602d86bf94e70fec9241fdfdf5dff93b4fbbc0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Tue, 27 Jun 2023 17:08:55 +0100 Subject: [PATCH 156/306] typo --- doc/2_quick_start_guide/Quick_Start_guide.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/2_quick_start_guide/Quick_Start_guide.rst b/doc/2_quick_start_guide/Quick_Start_guide.rst index cdd1933c..0192c7c5 100644 --- a/doc/2_quick_start_guide/Quick_Start_guide.rst +++ b/doc/2_quick_start_guide/Quick_Start_guide.rst @@ -19,8 +19,7 @@ Software requirements: - CMake, Ninja (Windows) - Python 3.7 or later -(Make sure all submodules are imported) - +Make sure all submodules are imported: .. code-block:: console git clone --recurse-submodules git@github.com:xmos/fwk_camera.git From 77e65ccba8eccbc45f5f44316c164b436b23a60f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Tue, 27 Jun 2023 17:09:56 +0100 Subject: [PATCH 157/306] typo --- doc/2_quick_start_guide/Quick_Start_guide.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/2_quick_start_guide/Quick_Start_guide.rst b/doc/2_quick_start_guide/Quick_Start_guide.rst index 0192c7c5..0cad33e6 100644 --- a/doc/2_quick_start_guide/Quick_Start_guide.rst +++ b/doc/2_quick_start_guide/Quick_Start_guide.rst @@ -20,6 +20,7 @@ Software requirements: - Python 3.7 or later Make sure all submodules are imported: + .. code-block:: console git clone --recurse-submodules git@github.com:xmos/fwk_camera.git From 3a9ccf2d9933d0ccbed5c93af64a3be4687ca131 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 27 Jun 2023 18:00:07 +0100 Subject: [PATCH 158/306] adding api for capturing [h][w][c] images --- camera/api/camera_api.h | 16 +++++++++-- camera/src/camera_api.c | 31 +++++++++++++++++++++- examples/take_picture_downsample/src/app.c | 13 ++++----- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/camera/api/camera_api.h b/camera/api/camera_api.h index f7892127..52a963ec 100644 --- a/camera/api/camera_api.h +++ b/camera/api/camera_api.h @@ -92,15 +92,27 @@ unsigned camera_capture_image_raw( /** * CLIENT SIDE * - * Called by the client to capture a decimated image. + * Called by the client to capture a decimated image in [channel][height][width] format. * * @param image_buff The buffer to store the image in * * @return Returns 0 on success, non-zero on failure */ -unsigned camera_capture_image( +unsigned camera_capture_image_transpose( int8_t image_buff[CH][H][W]); +/** + * CLIENT SIDE + * + * Called by the client to capture a decimated image in [height][width][channel] format. + * + * @param image_buff The buffer to store the image in + * + * @return Returns 0 on success, non-zero on failure + */ +unsigned camera_capture_image( + int8_t image_buff[H][W][CH]); + typedef struct { struct { unsigned row; diff --git a/camera/src/camera_api.c b/camera/src/camera_api.c index 973cebde..4aa31e69 100644 --- a/camera/src/camera_api.c +++ b/camera/src/camera_api.c @@ -115,7 +115,7 @@ unsigned camera_capture_image_raw( return 0; } -unsigned camera_capture_image( +unsigned camera_capture_image_transpose( int8_t image_buff[CH][H][W]) { unsigned row_index; @@ -144,6 +144,35 @@ unsigned camera_capture_image( return 0; } +unsigned camera_capture_image( + int8_t image_buff[H][W][CH]) +{ + unsigned row_index; + + int8_t pixel_data[CH][W]; + + // Loop, capturing rows until we get one with row_index==0 + do { + row_index = camera_capture_row_decimated(pixel_data); + } while (row_index != 0); + + for(int i = 0; i < W; i++) + for(int c = 0; c < CH; c++) + image_buff[0][i][c] = pixel_data[c][i]; + + // Now capture the rest of the rows + for (unsigned row = 1; row < H; row++) { + row_index = camera_capture_row_decimated(pixel_data); + + if (row_index != row) return 1; // TODO handle errors better + + for(int i = 0; i < W; i++) + for(int c = 0; c < CH; c++) + image_buff[row][i][c] = pixel_data[c][i]; + + } +} + unsigned camera_capture_image_cropped( int8_t* image_buff, const image_crop_params_t crop_params) diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index 360d194d..8f9a600d 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -11,8 +11,9 @@ void user_app() // Initialize camera api camera_init(); - int8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; - uint8_t temp_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; + //int8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; + int8_t image_buffer[APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS][APP_IMAGE_CHANNEL_COUNT]; + //uint8_t temp_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; // set the input image to 0 memset(image_buffer, -128, sizeof(image_buffer)); @@ -37,23 +38,23 @@ void user_app() (int8_t*) image_buffer, sizeof(image_buffer)); - memcpy(temp_buffer, image_buffer, APP_IMAGE_CHANNEL_COUNT * APP_IMAGE_HEIGHT_PIXELS * APP_IMAGE_WIDTH_PIXELS * sizeof(uint8_t)); + //memcpy(temp_buffer, image_buffer, APP_IMAGE_CHANNEL_COUNT * APP_IMAGE_HEIGHT_PIXELS * APP_IMAGE_WIDTH_PIXELS * sizeof(uint8_t)); uint8_t * io_buff = (uint8_t *) &image_buffer[0][0][0]; // io_buff this will have [APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS][APP_IMAGE_CHANNEL_COUNT] dimentions // apply gamma correction #if APPLY_GAMMA - isp_gamma((uint8_t *) &temp_buffer[0][0][0], + isp_gamma((uint8_t *) io_buff,//&temp_buffer[0][0][0], &gamma_new[0], APP_IMAGE_HEIGHT_PIXELS, APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_CHANNEL_COUNT); #endif - swap_dimensions((uint8_t *) &temp_buffer[0][0][0], io_buff, + /*swap_dimensions((uint8_t *) &temp_buffer[0][0][0], io_buff, APP_IMAGE_HEIGHT_PIXELS, APP_IMAGE_WIDTH_PIXELS, - APP_IMAGE_CHANNEL_COUNT); + APP_IMAGE_CHANNEL_COUNT);*/ // Write binary file write_image_file("capture.bin", io_buff, From 4f54cb7c17daedbf41111aae0ed5c2cb899d3445 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Tue, 27 Jun 2023 18:21:45 +0100 Subject: [PATCH 159/306] missing return --- camera/src/camera_api.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/camera/src/camera_api.c b/camera/src/camera_api.c index 4aa31e69..061ace50 100644 --- a/camera/src/camera_api.c +++ b/camera/src/camera_api.c @@ -171,6 +171,8 @@ unsigned camera_capture_image( image_buff[row][i][c] = pixel_data[c][i]; } + + return 0; } unsigned camera_capture_image_cropped( From 3545886a2e0ef98bb85cb9742809fd910ac3570e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 28 Jun 2023 09:50:33 +0100 Subject: [PATCH 160/306] cleaning examples readme --- doc/2_quick_start_guide/Quick_Start_guide.rst | 22 ++++++++--- examples/README.rst | 11 +++++- examples/take_picture_downsample/README.rst | 32 +++++----------- examples/take_picture_raw/README.rst | 31 +++++---------- utils/README.rst | 38 +++++++++++++++++++ 5 files changed, 85 insertions(+), 49 deletions(-) create mode 100644 utils/README.rst diff --git a/doc/2_quick_start_guide/Quick_Start_guide.rst b/doc/2_quick_start_guide/Quick_Start_guide.rst index 0cad33e6..f6a5dd0b 100644 --- a/doc/2_quick_start_guide/Quick_Start_guide.rst +++ b/doc/2_quick_start_guide/Quick_Start_guide.rst @@ -18,6 +18,7 @@ Software requirements: - XMOS tools: `SW_TOOLS`_ - CMake, Ninja (Windows) - Python 3.7 or later +- Xscope_fileio Make sure all submodules are imported: @@ -28,16 +29,27 @@ Make sure all submodules are imported: Run the RAW camera demo ^^^^^^^^^^^^^^^^^^^^^^^ This demo uses the RAW camera module to capture a RAW8 image and save it to a .raw file. -Then this image can be decoded using the `RAW image decoder`_. +Then, this image can be decoded using the python script `python decode_RAW8.py`. 1. Make sure that the camera is connecte to the board 2. Connect Power Supply and JTAG debugger -3. Build he example using the following commands: +3. Build the example using the following commands: -.. code-block:: console - cmake -G Ninja -B build --toolchain=xmos_cmake_toolchain\xs3a.cmake - ninja -C build example_take_picture_raw +.. tab:: Linux and Mac + + .. code-block:: console + + cmake -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake + make -C build example_take_picture_raw + +.. tab:: Windows + + .. code-block:: console + + cmake -G Ninja -B build --toolchain=xmos_cmake_toolchain\xs3a.cmake + ninja -C build example_take_picture_downsample + 4. Run the example using the following command: diff --git a/examples/README.rst b/examples/README.rst index 8ad1dfb5..cfa4fd2f 100644 --- a/examples/README.rst +++ b/examples/README.rst @@ -1 +1,10 @@ -//TODO +Examples +=========== + +This folder contains two examples for taking pictures: "take_picture_downsample" and "take_picture_raw". + +.. toctree:: + :maxdepth: 1 + + take_picture_downsample/README + take_picture_raw/README diff --git a/examples/take_picture_downsample/README.rst b/examples/take_picture_downsample/README.rst index e2cde567..4c9ad56d 100644 --- a/examples/take_picture_downsample/README.rst +++ b/examples/take_picture_downsample/README.rst @@ -11,16 +11,18 @@ Build example ************* Run the following commands from the top level: -.. tab:: Linux and Mac +Linux, Mac +~~~~~~~~~~ - .. code-block:: console +.. code-block:: console cmake -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake make -C build example_take_picture_downsample -.. tab:: Windows +Windows +~~~~~~~ - .. code-block:: console +.. code-block:: console cmake -G "Ninja" -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake ninja -C build example_take_picture_downsample @@ -29,26 +31,12 @@ Run the following commands from the top level: Running example *************** -From the top level +From the top level. +Make sure ``xscope_fileio`` is installed. See /utils/README.rst section for more details. -.. tab:: Linux and Mac +.. code-block:: console - .. code-block:: console - - pip install -e utils/xscope_fileio - cd python - python run_xscope_bin.py ../build/examples/take_picture_downsample/example_take_picture_downsample.xe - -.. tab:: Windows - - .. code-block:: console - - # works with a cl compiler - pip install -e utils\xscope_fileio - cd utils\xscope_fileio\host - cmake -G "Ninja" . && ninja - cd ..\..\..\python - python run_xscope_bin.py ..\build\examples\take_picture_downsample\example_take_picture_downsample.xe + python python/run_xscope_bin.py build/examples/take_picture_downsample/example_take_picture_downsample.xe ****** Output diff --git a/examples/take_picture_raw/README.rst b/examples/take_picture_raw/README.rst index f608fece..db3ac009 100644 --- a/examples/take_picture_raw/README.rst +++ b/examples/take_picture_raw/README.rst @@ -11,16 +11,18 @@ Build example ************* Run the following commands from the top level: -.. tab:: Linux and Mac +Linux, Mac +~~~~~~~~~~ - .. code-block:: console +.. code-block:: console cmake -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake make -C build example_take_picture_raw -.. tab:: Windows +Windows +~~~~~~~ - .. code-block:: console +.. code-block:: console cmake -G "Ninja" -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake ninja -C build example_take_picture_raw @@ -30,25 +32,12 @@ Running example *************** From the top level +Make sure ``xscope_fileio`` is installed. See /utils/README.rst section for more details. -.. tab:: Linux and Mac +.. code-block:: console + + python python/run_xscope_bin.py build/examples/take_picture_downsample/example_take_picture_downsample.xe - .. code-block:: console - - pip install -e utils/xscope_fileio - cd python - python run_xscope_bin.py ../build/examples/take_picture_raw/example_take_picture_raw.xe - -.. tab:: Windows - - .. code-block:: console - - # works with a cl compiler - pip install -e utils\xscope_fileio - cd utils\xscope_fileio\host - cmake -G "Ninja" . && ninja - cd ..\..\..\python - python run_xscope_bin.py ..\build\examples\take_picture_raw\example_take_picture_raw.xe ****** Output diff --git a/utils/README.rst b/utils/README.rst new file mode 100644 index 00000000..65c50433 --- /dev/null +++ b/utils/README.rst @@ -0,0 +1,38 @@ +Installing xscope_fileio +======================= + +To install `xscope_fileio`, please follow the steps below: + +1. Make sure you have a C/C++ compiler (e.g., cl compiler) installed. + +2. Open a terminal or command prompt. + +3. Install the package using pip by executing the following command: + +.. code-block:: console + + pip install -e utils\xscope_fileio + +4. Navigate to the host directory by executing the following command: + +.. code-block:: console + + cd utils\xscope_fileio\host + +5. Generate the build system files using CMake and build the binary: + +.. tab:: Linux and Mac + + .. code-block:: console + + cmake . + make + +.. tab:: Windows + + .. code-block:: console + + cmake -G "Ninja" . + ninja + +Your xscope_fileio host app is now ready to use. From e5d535bcf0709eff286b50c7b3d75f441b6d885c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 28 Jun 2023 09:53:39 +0100 Subject: [PATCH 161/306] windows mac rendering --- utils/README.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/utils/README.rst b/utils/README.rst index 65c50433..fa2d5dfe 100644 --- a/utils/README.rst +++ b/utils/README.rst @@ -21,16 +21,19 @@ To install `xscope_fileio`, please follow the steps below: 5. Generate the build system files using CMake and build the binary: -.. tab:: Linux and Mac - .. code-block:: console +Linux, Mac +~~~~~~~~~~ + +.. code-block:: console cmake . make -.. tab:: Windows +Windows +~~~~~~~ - .. code-block:: console +.. code-block:: console cmake -G "Ninja" . ninja From a9944a77de794fa8594f9005dd5bc1becce7af7d Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Wed, 28 Jun 2023 10:42:13 +0100 Subject: [PATCH 162/306] removing uncommented stuff --- examples/take_picture_downsample/src/app.c | 30 ++++++++-------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index 8f9a600d..581479e3 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -11,9 +11,7 @@ void user_app() // Initialize camera api camera_init(); - //int8_t image_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; int8_t image_buffer[APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS][APP_IMAGE_CHANNEL_COUNT]; - //uint8_t temp_buffer[APP_IMAGE_CHANNEL_COUNT][APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS]; // set the input image to 0 memset(image_buffer, -128, sizeof(image_buffer)); @@ -32,38 +30,30 @@ void user_app() camera_stop(); delay_milliseconds(100); - // convert to uint8 with right dimentions // convert to uint8 vect_int8_to_uint8((uint8_t*) image_buffer, - (int8_t*) image_buffer, + image_buffer, sizeof(image_buffer)); - - //memcpy(temp_buffer, image_buffer, APP_IMAGE_CHANNEL_COUNT * APP_IMAGE_HEIGHT_PIXELS * APP_IMAGE_WIDTH_PIXELS * sizeof(uint8_t)); - uint8_t * io_buff = (uint8_t *) &image_buffer[0][0][0]; - // io_buff this will have [APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS][APP_IMAGE_CHANNEL_COUNT] dimentions // apply gamma correction #if APPLY_GAMMA - isp_gamma((uint8_t *) io_buff,//&temp_buffer[0][0][0], - &gamma_new[0], - APP_IMAGE_HEIGHT_PIXELS, - APP_IMAGE_WIDTH_PIXELS, - APP_IMAGE_CHANNEL_COUNT); + isp_gamma((uint8_t *) image_buffer, + &gamma_new[0], + APP_IMAGE_HEIGHT_PIXELS, + APP_IMAGE_WIDTH_PIXELS, + APP_IMAGE_CHANNEL_COUNT); #endif - - /*swap_dimensions((uint8_t *) &temp_buffer[0][0][0], io_buff, - APP_IMAGE_HEIGHT_PIXELS, - APP_IMAGE_WIDTH_PIXELS, - APP_IMAGE_CHANNEL_COUNT);*/ // Write binary file - write_image_file("capture.bin", io_buff, + write_image_file("capture.bin", + (uint8_t *) image_buffer, APP_IMAGE_HEIGHT_PIXELS, APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_CHANNEL_COUNT); // Write bmp file - write_bmp_file("capture.bmp", io_buff, + write_bmp_file("capture.bmp", + (uint8_t *) image_buffer, APP_IMAGE_HEIGHT_PIXELS, APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_CHANNEL_COUNT); From 44942d889b3bc6d069b46a40454e24e94178ff3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 28 Jun 2023 10:49:46 +0100 Subject: [PATCH 163/306] typo --- doc/2_quick_start_guide/Quick_Start_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/2_quick_start_guide/Quick_Start_guide.rst b/doc/2_quick_start_guide/Quick_Start_guide.rst index f6a5dd0b..1a225e18 100644 --- a/doc/2_quick_start_guide/Quick_Start_guide.rst +++ b/doc/2_quick_start_guide/Quick_Start_guide.rst @@ -29,7 +29,7 @@ Make sure all submodules are imported: Run the RAW camera demo ^^^^^^^^^^^^^^^^^^^^^^^ This demo uses the RAW camera module to capture a RAW8 image and save it to a .raw file. -Then, this image can be decoded using the python script `python decode_RAW8.py`. +Then, this image can be decoded using the python script ``python decode_RAW8.py``. 1. Make sure that the camera is connecte to the board 2. Connect Power Supply and JTAG debugger From 82ff2097aa7623ed22bd7568f238974f523be566 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Wed, 28 Jun 2023 10:49:59 +0100 Subject: [PATCH 164/306] build warnings --- examples/take_picture_downsample/src/app.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index 581479e3..ed7955f8 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -12,6 +12,7 @@ void user_app() camera_init(); int8_t image_buffer[APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS][APP_IMAGE_CHANNEL_COUNT]; + uint8_t * image_ptr = (uint8_t *) &image_buffer[0][0][0]; // set the input image to 0 memset(image_buffer, -128, sizeof(image_buffer)); @@ -31,13 +32,13 @@ void user_app() delay_milliseconds(100); // convert to uint8 - vect_int8_to_uint8((uint8_t*) image_buffer, - image_buffer, + vect_int8_to_uint8(image_ptr, + &image_buffer[0][0][0], sizeof(image_buffer)); // apply gamma correction #if APPLY_GAMMA - isp_gamma((uint8_t *) image_buffer, + isp_gamma(image_ptr, &gamma_new[0], APP_IMAGE_HEIGHT_PIXELS, APP_IMAGE_WIDTH_PIXELS, @@ -46,14 +47,14 @@ void user_app() // Write binary file write_image_file("capture.bin", - (uint8_t *) image_buffer, + image_ptr, APP_IMAGE_HEIGHT_PIXELS, APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_CHANNEL_COUNT); // Write bmp file write_bmp_file("capture.bmp", - (uint8_t *) image_buffer, + image_ptr, APP_IMAGE_HEIGHT_PIXELS, APP_IMAGE_WIDTH_PIXELS, APP_IMAGE_CHANNEL_COUNT); From 39f329cfd1f0e10313cddfc8ac05a597cdb274a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 28 Jun 2023 12:05:21 +0100 Subject: [PATCH 165/306] solving pr comments --- doc/1_programming_guide/01_Introduction.rst | 2 +- .../02_Architecture_and_Design.rst | 10 ++-- .../03_Building_and_testing_the_Software.rst | 2 +- doc/2_quick_start_guide/Quick_Start_guide.rst | 19 +++---- doc/4_release_notes/release_notes.rst | 50 +++++++------------ examples/take_picture_raw/README.rst | 2 +- utils/README.rst | 29 ++++------- 7 files changed, 46 insertions(+), 68 deletions(-) diff --git a/doc/1_programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst index dee0b334..da376b1f 100644 --- a/doc/1_programming_guide/01_Introduction.rst +++ b/doc/1_programming_guide/01_Introduction.rst @@ -38,7 +38,7 @@ This repository contains a set of tools for image acquisition, processing, and t The architecture, viewed from a high level, is composed of the following elements: .. figure:: images/1_high_level_view.png - :alt: Alternate text for the image + :alt: High-level block diagram :align: center High-level block diagram of the FWK_Camera. diff --git a/doc/1_programming_guide/02_Architecture_and_Design.rst b/doc/1_programming_guide/02_Architecture_and_Design.rst index 18e6ff0c..cf389642 100644 --- a/doc/1_programming_guide/02_Architecture_and_Design.rst +++ b/doc/1_programming_guide/02_Architecture_and_Design.rst @@ -13,17 +13,17 @@ Hardware Architecture ---------------------- From a Hardware point of view, fwk_camera is composed of the following modules: -#. Camera connector : 15-PIN MIPI CSI connector for the camera modul#. (compatible with Raspberry Pi). -#. Mipi Shim : Xmos MIPI hardware to convert MIPI signals to Xcore p#.ts. -#. Xcore : Xmos Xcore processor to process the MIPI signal#. -#. Jtag Host connector : USB connector to connect to the h#.t. +#. Camera connector : 15-PIN MIPI CSI connector for the camera module. (compatible with Raspberry Pi). +#. Mipi Shim : Xmos MIPI hardware to convert MIPI signals to Xcore ports. +#. Xcore : Xmos Xcore processor to process the MIPI signals. +#. Jtag Host connector : USB connector to connect to the host. Software Architecture ------------------------- From a Software point of view, fwk_camera is composed of the following modules: .. figure:: images/2_object_diagram.svg - :alt: Alternative Text + :alt: fwk_camera object diagram :figclass: custom-class Object Diagram diff --git a/doc/1_programming_guide/03_Building_and_testing_the_Software.rst b/doc/1_programming_guide/03_Building_and_testing_the_Software.rst index 08a4ea6a..eff18aea 100644 --- a/doc/1_programming_guide/03_Building_and_testing_the_Software.rst +++ b/doc/1_programming_guide/03_Building_and_testing_the_Software.rst @@ -4,7 +4,7 @@ Building the Software .. include:: ../substitutions.rst This section will provide details on how the software is constructed. The basic steps and build requirements -can be found in the README.md file which is distributed with the source. +can be found in the README.rst file which is distributed with the source. Requirements diff --git a/doc/2_quick_start_guide/Quick_Start_guide.rst b/doc/2_quick_start_guide/Quick_Start_guide.rst index 1a225e18..543a41f7 100644 --- a/doc/2_quick_start_guide/Quick_Start_guide.rst +++ b/doc/2_quick_start_guide/Quick_Start_guide.rst @@ -9,16 +9,15 @@ Hardware requirements: ^^^^^^^^^^^^^^^^^^^^^^^ - XCORE.AI EVALUATION KIT (XK-EVK-XU316) - Camera module -- Power supply -- Micro USB cable -- JTAG debugger +- Camera ribbon connector +- 2xMicro USB cable (Power supply and Jtag) +- JTAG debugger and cable Software requirements: ^^^^^^^^^^^^^^^^^^^^^^^ - XMOS tools: `SW_TOOLS`_ - CMake, Ninja (Windows) - Python 3.7 or later -- Xscope_fileio Make sure all submodules are imported: @@ -29,12 +28,14 @@ Make sure all submodules are imported: Run the RAW camera demo ^^^^^^^^^^^^^^^^^^^^^^^ This demo uses the RAW camera module to capture a RAW8 image and save it to a .raw file. -Then, this image can be decoded using the python script ``python decode_RAW8.py``. +Then, this image can be decoded using the python script ``python decode_raw8.py``. -1. Make sure that the camera is connecte to the board -2. Connect Power Supply and JTAG debugger -3. Build the example using the following commands: +.. note:: + Make sure xscope_fileio is installed. See /utils/README.rst section for more details +#. Ensure that the camera is connected to the board +#. Connect Power Supply and JTAG debugger +#. Build the example using the following commands: .. tab:: Linux and Mac @@ -48,7 +49,7 @@ Then, this image can be decoded using the python script ``python decode_RAW8.py` .. code-block:: console cmake -G Ninja -B build --toolchain=xmos_cmake_toolchain\xs3a.cmake - ninja -C build example_take_picture_downsample + ninja -C build example_take_picture_raw 4. Run the example using the following command: diff --git a/doc/4_release_notes/release_notes.rst b/doc/4_release_notes/release_notes.rst index f1f638ca..e9f9962a 100644 --- a/doc/4_release_notes/release_notes.rst +++ b/doc/4_release_notes/release_notes.rst @@ -1,47 +1,31 @@ Release Notes ============= -Version 0.0.1 (YYYY-MM-DD) +Version 0.0.1 --------------------------- -New Features: +Description: ************* +This is a initital release of the fwk_camera repo. It contains a basic interface for adquiring images, process them and send them to the host. +It also contains a basic interface for controlling the camera ISP features. -- [Feature 1]: [Description] -- [Feature 2]: [Description] -- ... - -Enhancements: +New Features: ************* -- [Enhancement 1]: [Description] -- [Enhancement 2]: [Description] -- ... - -Bug Fixes: -****************** - -- [Bug Fix 1]: [Description] -- [Bug Fix 2]: [Description] -- ... - -Changes: -****************** - -- [Change 1]: [Description] -- [Change 2]: [Description] -- ... - -Deprecations and Removals: -****************************** +- Raw8 capture: Capture a raw-8 image from the camera and send it to the host. +- Raw8 capture and downsample: capture a raw image from the camera, downsample it by 4 and send it to the host. +- Raw10 mode: mode to capture raw10 images from the camera. +- ISP features: + - AE control + - AWB control + - Gamma correction + - Image rotation + - Image cropping / scaling -- [Deprecation 1]: [Description] -- [Deprecation 2]: [Description] -- ... Known Issues: ************* -- [Issue 1]: [Description] -- [Issue 2]: [Description] -- ... +- RAW10 downsample: +- Artifacts: +- Camera binning modes: diff --git a/examples/take_picture_raw/README.rst b/examples/take_picture_raw/README.rst index db3ac009..313f57da 100644 --- a/examples/take_picture_raw/README.rst +++ b/examples/take_picture_raw/README.rst @@ -36,7 +36,7 @@ Make sure ``xscope_fileio`` is installed. See /utils/README.rst section for more .. code-block:: console - python python/run_xscope_bin.py build/examples/take_picture_downsample/example_take_picture_downsample.xe + python python/run_xscope_bin.py build/examples/take_picture_raw/example_take_picture_raw.xe ****** diff --git a/utils/README.rst b/utils/README.rst index fa2d5dfe..5f52b7bb 100644 --- a/utils/README.rst +++ b/utils/README.rst @@ -7,11 +7,21 @@ To install `xscope_fileio`, please follow the steps below: 2. Open a terminal or command prompt. -3. Install the package using pip by executing the following command: +3. Install the xscope_fileio package: + +Linux, Mac +~~~~~~~~~~ .. code-block:: console + + pip install -e utils\xscope_fileio + +Windows +~~~~~~~ +.. code-block:: console pip install -e utils\xscope_fileio + cmake -G "Ninja" . && ninja 4. Navigate to the host directory by executing the following command: @@ -21,21 +31,4 @@ To install `xscope_fileio`, please follow the steps below: 5. Generate the build system files using CMake and build the binary: - -Linux, Mac -~~~~~~~~~~ - -.. code-block:: console - - cmake . - make - -Windows -~~~~~~~ - -.. code-block:: console - - cmake -G "Ninja" . - ninja - Your xscope_fileio host app is now ready to use. From 68da28d9db7960915405f7e8b10f54557cfc622e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 28 Jun 2023 14:48:45 +0100 Subject: [PATCH 166/306] doc corrections --- camera/api/isp.h | 4 ++-- doc/4_release_notes/release_notes.rst | 22 ++++++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/camera/api/isp.h b/camera/api/isp.h index 39885734..e3d24ef1 100644 --- a/camera/api/isp.h +++ b/camera/api/isp.h @@ -9,9 +9,9 @@ // ISP settings #define AE_MARGIN 0.1 // default marging for the auto exposure error #define AE_INITIAL_EXPOSURE 35 // initial exposure value -#define AWB_gain_RED 1 +#define AWB_gain_RED 1.3 #define AWB_gain_GREEN 1 -#define AWB_gain_BLUE 1 +#define AWB_gain_BLUE 1.3 #define AWB_MAX 1.7 #define AWB_MIN 0.8 #define APPLY_GAMMA 1 diff --git a/doc/4_release_notes/release_notes.rst b/doc/4_release_notes/release_notes.rst index e9f9962a..3df815a8 100644 --- a/doc/4_release_notes/release_notes.rst +++ b/doc/4_release_notes/release_notes.rst @@ -16,16 +16,22 @@ New Features: - Raw8 capture and downsample: capture a raw image from the camera, downsample it by 4 and send it to the host. - Raw10 mode: mode to capture raw10 images from the camera. - ISP features: - - AE control - - AWB control - - Gamma correction - - Image rotation - - Image cropping / scaling + - AE control: auto exposure based on histogram skewness + - AWB control: auto white balance based on a combination of grey world assumption and percentile + - Gamma correction: 1.8 gamma correction. + - Image rotation: image rotation capabilities (sensor and ISP) + - Image cropping / scaling : image scalling, cropping. +Limitations: +************* +- RAW10 downsample: RAW10 downsample is not supported. +- Galaxy core integration it is not supported yet. Known Issues: ************* - -- RAW10 downsample: - Artifacts: -- Camera binning modes: + - AE: Auto exposure can struggle in enviornement with high dynamic range. + It will choose to privilege the mayority of the image, leaving some areas underexposed or overexposed. + - AWB: Due to white balancing algorithmm, we can observe that + pure color pure red or pure blue can appear more white than expected. + AWB can me turned off, or changed manually to adequate to a specific scene. From 1931b46a2bbd875113fffd9afc30041e0af05775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 28 Jun 2023 14:51:43 +0100 Subject: [PATCH 167/306] release notes fix --- doc/4_release_notes/release_notes.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/4_release_notes/release_notes.rst b/doc/4_release_notes/release_notes.rst index 3df815a8..d5a0cd06 100644 --- a/doc/4_release_notes/release_notes.rst +++ b/doc/4_release_notes/release_notes.rst @@ -30,8 +30,9 @@ Limitations: Known Issues: ************* - Artifacts: - - AE: Auto exposure can struggle in enviornement with high dynamic range. + - AE: + Auto exposure can struggle in enviornement with high dynamic range. It will choose to privilege the mayority of the image, leaving some areas underexposed or overexposed. - - AWB: Due to white balancing algorithmm, we can observe that - pure color pure red or pure blue can appear more white than expected. + - AWB: + Due to white balancing algorithmm, we can observe that pure color pure red or pure blue can appear more white than expected. AWB can me turned off, or changed manually to adequate to a specific scene. From 79dac6de883c1b938cf0a4dc75ef718c1c4b0915 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Wed, 28 Jun 2023 15:09:36 +0100 Subject: [PATCH 168/306] defining format in the top level cmake --- CMakeLists.txt | 4 ++++ examples/CMakeLists.txt | 6 +++++- examples/take_picture_raw/src/app_raw.c | 2 +- sensors/CMakeLists.txt | 5 +++++ sensors/api/sensor.h | 7 ++++--- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index aef16b6a..46034604 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,10 @@ string(REPLACE "-MD" "-MMD" CMAKE_DEPFILE_FLAGS_C ${CMAKE_DEPFILE_FLAGS_C}) if(${CMAKE_SYSTEM_PROCESSOR} STREQUAL XCORE_XS3A) +set(RAW8 0x2A) +set(RAW10 0x2B) +set(CONFIG_MIPI_FORMAT ${RAW8}) + ## Add frameworks add_subdirectory(modules) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index cdf3408b..41e7f7bb 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -31,4 +31,8 @@ set(EXAMPLE_APP_LINK_LIBRARIES # add the examples add_subdirectory(take_picture_raw) -add_subdirectory(take_picture_downsample) + +if(${CONFIG_MIPI_FORMAT} STREQUAL ${RAW8}) + # we only support RAW8 images in this example for now + add_subdirectory(take_picture_downsample) +endif() diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index f0f2273a..28a89967 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -40,7 +40,7 @@ void user_app_raw() // Save the image to a file write_image_file("capture.raw", (uint8_t * ) &image_buffer[0][0], - MIPI_IMAGE_HEIGHT_PIXELS, MIPI_LINE_WIDTH_BYTES, 1); + MIPI_IMAGE_HEIGHT_PIXELS, MIPI_IMAGE_WIDTH_BYTES, 1); printf("Image saved. Exiting.\n"); xscope_close_all_files(); diff --git a/sensors/CMakeLists.txt b/sensors/CMakeLists.txt index 3a77bb80..2a38a834 100644 --- a/sensors/CMakeLists.txt +++ b/sensors/CMakeLists.txt @@ -17,6 +17,11 @@ file(GLOB_RECURSE SOURCES_ASM "*.S") add_library(${LIB_NAME} STATIC) +target_compile_definitions(${LIB_NAME} + PUBLIC + CONFIG_MIPI_FORMAT=${CONFIG_MIPI_FORMAT} +) + target_include_directories(${LIB_NAME} PUBLIC api diff --git a/sensors/api/sensor.h b/sensors/api/sensor.h index 23a15597..39023c14 100644 --- a/sensors/api/sensor.h +++ b/sensors/api/sensor.h @@ -16,7 +16,9 @@ #define CONFIG_MODE MODE_VGA_640x480 // Mipi format and mode +#ifndef CONFIG_MIPI_FORMAT #define CONFIG_MIPI_FORMAT MIPI_DT_RAW8 +#endif #define MIPI_PKT_BUFFER_COUNT 4 // FPS settings @@ -107,8 +109,7 @@ // ----------------------- Settings dependant of each sensor library // Camera dependant (do not edit) -#define MIPI_LINE_WIDTH_BYTES MIPI_IMAGE_WIDTH_BYTES -#define MIPI_MAX_PKT_SIZE_BYTES ((MIPI_LINE_WIDTH_BYTES) + 4) +#define MIPI_MAX_PKT_SIZE_BYTES ((MIPI_IMAGE_WIDTH_BYTES) + 4) #define MIPI_TILE 1 #define EXPECTED_FORMAT CONFIG_MIPI_FORMAT //backward compatibility #define MIPI_EXPECTED_FORMAT CONFIG_MIPI_FORMAT //backward compatibility @@ -165,4 +166,4 @@ #define W (APP_IMAGE_WIDTH_PIXELS) #define H_RAW (MIPI_IMAGE_HEIGHT_PIXELS) -#define W_RAW (MIPI_IMAGE_WIDTH_PIXELS) +#define W_RAW (MIPI_IMAGE_WIDTH_BYTES) From c4818505482ead934157e727faa99ed3485efba0 Mon Sep 17 00:00:00 2001 From: uvvpavel Date: Wed, 28 Jun 2023 15:31:24 +0100 Subject: [PATCH 169/306] more formal names --- CMakeLists.txt | 6 +++--- examples/CMakeLists.txt | 2 +- python/decode_raw10.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 46034604..04d57ff5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,9 +19,9 @@ string(REPLACE "-MD" "-MMD" CMAKE_DEPFILE_FLAGS_C ${CMAKE_DEPFILE_FLAGS_C}) if(${CMAKE_SYSTEM_PROCESSOR} STREQUAL XCORE_XS3A) -set(RAW8 0x2A) -set(RAW10 0x2B) -set(CONFIG_MIPI_FORMAT ${RAW8}) +set(CONFIG_RAW8 0x2A) +set(CONFIG_RAW10 0x2B) +set(CONFIG_MIPI_FORMAT ${CONFIG_RAW8}) ## Add frameworks add_subdirectory(modules) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 41e7f7bb..d7cb55b9 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -32,7 +32,7 @@ set(EXAMPLE_APP_LINK_LIBRARIES # add the examples add_subdirectory(take_picture_raw) -if(${CONFIG_MIPI_FORMAT} STREQUAL ${RAW8}) +if(${CONFIG_MIPI_FORMAT} STREQUAL ${CONFIG_RAW8}) # we only support RAW8 images in this example for now add_subdirectory(take_picture_downsample) endif() diff --git a/python/decode_raw10.py b/python/decode_raw10.py index 5c7257a9..34dc36ed 100644 --- a/python/decode_raw10.py +++ b/python/decode_raw10.py @@ -24,7 +24,7 @@ plot_imgs ) -input_name = os.getenv('BINARY_IMG_PATH') +input_name = os.getenv('BINARY_IMG_PATH') or "capture.raw" width = 640 height = 480 @@ -32,8 +32,8 @@ flip = False as_shot_neutral = [0.5666090846, 1, 0.7082979679] as_shot_neutral = [0.6301882863, 1, 0.6555861831] -#cfa_pattern = [0, 1, 1, 2] -cfa_pattern = [2, 1, 1, 0] +cfa_pattern = [0, 1, 1, 2] +#cfa_pattern = [2, 1, 1, 0] # read the data with open(input_name, "rb") as f: From 7c0324a46a94cbfad75ebdedbab73e83222d8f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 28 Jun 2023 15:39:23 +0100 Subject: [PATCH 170/306] doc review --- CHANGELOG.rst | 14 +++++++++++++ .../02_Architecture_and_Design.rst | 2 +- doc/2_quick_start_guide/Quick_Start_guide.rst | 6 +++--- doc/4_release_notes/release_notes.rst | 20 +++++++------------ python/decode_raw10.py | 6 +++--- utils/README.rst | 13 ++++-------- 6 files changed, 32 insertions(+), 29 deletions(-) create mode 100644 CHANGELOG.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 00000000..c08a026f --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,14 @@ +fwk_camera change log +===================== + +0.1.0 +----- + +* ADDED: Sony IMX219 support (Raspberry Pi Camera V2) +* ADDED: Raw8 capture example (640X480) +* ADDED: Raw8 + Downsample exmaple (160x120x3) +* ADDED: AE control: auto exposure based on histogram skewness +* ADDED: AWB control: auto white balance. Combination of grey world assumption and percentile white balance. +* ADDED: AWB control: auto white balance based on a combination of grey world assumption and percentile +* ADDED: Image rotation: image rotation capabilities (sensor and ISP) +* ADDED: cropping / scaling : image scalling, cropping. diff --git a/doc/1_programming_guide/02_Architecture_and_Design.rst b/doc/1_programming_guide/02_Architecture_and_Design.rst index cf389642..0c53e6b1 100644 --- a/doc/1_programming_guide/02_Architecture_and_Design.rst +++ b/doc/1_programming_guide/02_Architecture_and_Design.rst @@ -16,7 +16,7 @@ From a Hardware point of view, fwk_camera is composed of the following modules: #. Camera connector : 15-PIN MIPI CSI connector for the camera module. (compatible with Raspberry Pi). #. Mipi Shim : Xmos MIPI hardware to convert MIPI signals to Xcore ports. #. Xcore : Xmos Xcore processor to process the MIPI signals. -#. Jtag Host connector : USB connector to connect to the host. +#. Xtag Host connector : USB connector to connect to the host. Software Architecture ------------------------- diff --git a/doc/2_quick_start_guide/Quick_Start_guide.rst b/doc/2_quick_start_guide/Quick_Start_guide.rst index 543a41f7..a822661f 100644 --- a/doc/2_quick_start_guide/Quick_Start_guide.rst +++ b/doc/2_quick_start_guide/Quick_Start_guide.rst @@ -10,8 +10,8 @@ Hardware requirements: - XCORE.AI EVALUATION KIT (XK-EVK-XU316) - Camera module - Camera ribbon connector -- 2xMicro USB cable (Power supply and Jtag) -- JTAG debugger and cable +- 2xMicro USB cable (Power supply and Xtag) +- Xtag debugger and cable Software requirements: ^^^^^^^^^^^^^^^^^^^^^^^ @@ -34,7 +34,7 @@ Then, this image can be decoded using the python script ``python decode_raw8.py` Make sure xscope_fileio is installed. See /utils/README.rst section for more details #. Ensure that the camera is connected to the board -#. Connect Power Supply and JTAG debugger +#. Connect Power Supply and Xtag debugger #. Build the example using the following commands: .. tab:: Linux and Mac diff --git a/doc/4_release_notes/release_notes.rst b/doc/4_release_notes/release_notes.rst index d5a0cd06..8fe265ff 100644 --- a/doc/4_release_notes/release_notes.rst +++ b/doc/4_release_notes/release_notes.rst @@ -1,7 +1,7 @@ Release Notes ============= -Version 0.0.1 +Version 0.1. --------------------------- Description: @@ -12,20 +12,14 @@ It also contains a basic interface for controlling the camera ISP features. New Features: ************* -- Raw8 capture: Capture a raw-8 image from the camera and send it to the host. -- Raw8 capture and downsample: capture a raw image from the camera, downsample it by 4 and send it to the host. -- Raw10 mode: mode to capture raw10 images from the camera. -- ISP features: - - AE control: auto exposure based on histogram skewness - - AWB control: auto white balance based on a combination of grey world assumption and percentile - - Gamma correction: 1.8 gamma correction. - - Image rotation: image rotation capabilities (sensor and ISP) - - Image cropping / scaling : image scalling, cropping. +In this first release we include two basic examples to capture an image from the Sony IMX219 sensor (Raspberry Pi Camera V2). +For the first examples the image is directly captured as raw and saved as a .raw file. +The second example is a more complex pipeline that involves the ISP. The image is captured as raw, processed by the ISP and saved as a .bmp and .bin file. Limitations: ************* - RAW10 downsample: RAW10 downsample is not supported. -- Galaxy core integration it is not supported yet. +- Galaxy core sensor pipeline it is not yet supported. Known Issues: ************* @@ -34,5 +28,5 @@ Known Issues: Auto exposure can struggle in enviornement with high dynamic range. It will choose to privilege the mayority of the image, leaving some areas underexposed or overexposed. - AWB: - Due to white balancing algorithmm, we can observe that pure color pure red or pure blue can appear more white than expected. - AWB can me turned off, or changed manually to adequate to a specific scene. + Due to automatic white balancing algorithmm ISP will try to compensante images iluminant. If the environement is a pure color pure red or pure blue, it can appear more white than expected. + In this case, AWB can me turned off, or changed manually to adequate to a specific scene adjusting the static AWB values. diff --git a/python/decode_raw10.py b/python/decode_raw10.py index 5c7257a9..55f1479d 100644 --- a/python/decode_raw10.py +++ b/python/decode_raw10.py @@ -32,8 +32,8 @@ flip = False as_shot_neutral = [0.5666090846, 1, 0.7082979679] as_shot_neutral = [0.6301882863, 1, 0.6555861831] -#cfa_pattern = [0, 1, 1, 2] -cfa_pattern = [2, 1, 1, 0] +#cfa_pattern = [2, 1, 1, 0] # raspberry +cfa_pattern = [0, 1, 1, 2] # explorer board # read the data with open(input_name, "rb") as f: @@ -69,7 +69,7 @@ img = new_color_correction(img) # gamma -img = img ** (1.0 / 2.2) +img = img ** (1.0 / 1.8) # clip the image img = np.clip(255*img, 0, 255).astype(np.uint8) diff --git a/utils/README.rst b/utils/README.rst index 5f52b7bb..8ee86b4b 100644 --- a/utils/README.rst +++ b/utils/README.rst @@ -14,21 +14,16 @@ Linux, Mac .. code-block:: console - pip install -e utils\xscope_fileio + pip install -e utils/xscope_fileio Windows ~~~~~~~ .. code-block:: console - pip install -e utils\xscope_fileio - cmake -G "Ninja" . && ninja - -4. Navigate to the host directory by executing the following command: - -.. code-block:: console - cd utils\xscope_fileio\host - + pip install -e utils/xscope_fileio + cmake -G "Ninja" . && ninja + 5. Generate the build system files using CMake and build the binary: Your xscope_fileio host app is now ready to use. From 0173cc15f1a5e1285247688706d7cc310efdba89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 28 Jun 2023 15:39:54 +0100 Subject: [PATCH 171/306] cangelog --- CHANGELOG.rst | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c08a026f..def5a308 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,11 +4,15 @@ fwk_camera change log 0.1.0 ----- -* ADDED: Sony IMX219 support (Raspberry Pi Camera V2) -* ADDED: Raw8 capture example (640X480) -* ADDED: Raw8 + Downsample exmaple (160x120x3) -* ADDED: AE control: auto exposure based on histogram skewness +* ADDED: Sony IMX219 support (Raspberry Pi Camera V2). +* ADDED: RAW8 and Raw10 capture support using only internal RAM. +* ADDED: Raw8 capture example (640X480). +* ADDED: Raw8 + Downsample exmaple (160x120x3) RGB. +* ADDED: Xscope_fileio suport. +* ADDED: AE control: auto exposure based on histogram skewness. * ADDED: AWB control: auto white balance. Combination of grey world assumption and percentile white balance. -* ADDED: AWB control: auto white balance based on a combination of grey world assumption and percentile +* ADDED: AWB control: auto white balance based on a combination of grey world assumption and percentile. * ADDED: Image rotation: image rotation capabilities (sensor and ISP) * ADDED: cropping / scaling : image scalling, cropping. +* ADDED: Image statistics: histogram, mean, variance, skewness. +* ADDED: sensor control, start, stop functons. From 013658b5f04070f3a4bd756dd5eb69cb12afc419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 28 Jun 2023 15:56:53 +0100 Subject: [PATCH 172/306] minor changes --- doc/4_release_notes/release_notes.rst | 2 +- settings.json | 2 +- utils/README.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/4_release_notes/release_notes.rst b/doc/4_release_notes/release_notes.rst index 8fe265ff..3e111703 100644 --- a/doc/4_release_notes/release_notes.rst +++ b/doc/4_release_notes/release_notes.rst @@ -1,7 +1,7 @@ Release Notes ============= -Version 0.1. +Version 0.1.0 --------------------------- Description: diff --git a/settings.json b/settings.json index a622df9b..46580ac8 100644 --- a/settings.json +++ b/settings.json @@ -1,5 +1,5 @@ { "title": "XMOS FWK Camera", "project": "fwk_camera", - "version": "0.1" + "version": "0.1.0" } diff --git a/utils/README.rst b/utils/README.rst index 8ee86b4b..f060075a 100644 --- a/utils/README.rst +++ b/utils/README.rst @@ -20,8 +20,8 @@ Windows ~~~~~~~ .. code-block:: console - cd utils\xscope_fileio\host pip install -e utils/xscope_fileio + cd utils/xscope_fileio/host cmake -G "Ninja" . && ninja 5. Generate the build system files using CMake and build the binary: From ce4420f97c9c1936118ddd7e492ab7091806dbfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Wed, 28 Jun 2023 17:06:00 +0100 Subject: [PATCH 173/306] review modifications --- doc/1_programming_guide/02_Architecture_and_Design.rst | 6 +++--- doc/4_release_notes/release_notes.rst | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/1_programming_guide/02_Architecture_and_Design.rst b/doc/1_programming_guide/02_Architecture_and_Design.rst index 0c53e6b1..685c2d60 100644 --- a/doc/1_programming_guide/02_Architecture_and_Design.rst +++ b/doc/1_programming_guide/02_Architecture_and_Design.rst @@ -30,9 +30,9 @@ From a Software point of view, fwk_camera is composed of the following modules: Module description: -#. camera section: the camera section takes care of start, process and stop the camera. It has to be aware of the sensor configuration (in sesnor section) and meet the user demands (in user section). -#. user section: the user section is the interface between the user and the camera, where we define what we want to do with the frames. -#. sensor section: configuration and control of the sensor. +#. Camera section: the camera section takes care of start, process and stop the camera. It has to be aware of the sensor configuration (in sesnor section) and meet the user demands (in user section). +#. User section: the user section is the interface between the user and the camera, where we define what we want to do with the frames. +#. Sensor section: configuration and control of the sensor. #. Host section: provide the interface to write the frames or files to the host. There are other sections not mentioned in the diagram, as the test section, which is used to test the camera. diff --git a/doc/4_release_notes/release_notes.rst b/doc/4_release_notes/release_notes.rst index 3e111703..3b7562de 100644 --- a/doc/4_release_notes/release_notes.rst +++ b/doc/4_release_notes/release_notes.rst @@ -6,7 +6,7 @@ Version 0.1.0 Description: ************* -This is a initital release of the fwk_camera repo. It contains a basic interface for adquiring images, process them and send them to the host. +This is a initital release of the fwk_camera repo. It contains a basic interface for acquiring images, process them and send them to the host. It also contains a basic interface for controlling the camera ISP features. New Features: From e1725abd770ee8b5d1e2f3e28cbca0ddf3506935 Mon Sep 17 00:00:00 2001 From: Brennan Magee Date: Thu, 29 Jun 2023 11:28:25 +0100 Subject: [PATCH 174/306] Add doc build to jenkins --- Jenkinsfile | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index d9c2f898..d5f856ec 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -20,7 +20,7 @@ pipeline { stages { stage ('Build and Unit test') { agent { - label 'linux&&x86_64' + label 'linux&&x86_64&&docker' } stages { @@ -36,6 +36,18 @@ pipeline { } } } // Build + + stage ('Build Docs') { + steps { + sh """docker run --user "$(id -u):$(id -g)" \ + --rm \ + -v ${WORKSPACE}:/build \ + -e EXCLUDE_PATTERNS="/build/doc/exclude-patterns.inc" \ + -e PDF=1 \ + ghcr.io/xmos/doc_builder:v3.0.0""" + archiveArtifacts artifacts: "doc/_build/**", allowEmptyArchive: true + } + } // Build Docs stage('Unit tests') { steps { From 80970155a295502cfe9067b2d6c64ae7159ac53b Mon Sep 17 00:00:00 2001 From: Brennan Magee Date: Thu, 29 Jun 2023 11:30:00 +0100 Subject: [PATCH 175/306] escaoe jenkins dollar signs --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index d5f856ec..266c9d22 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -39,7 +39,7 @@ pipeline { stage ('Build Docs') { steps { - sh """docker run --user "$(id -u):$(id -g)" \ + sh """docker run --user "\$(id -u):\$(id -g)" \ --rm \ -v ${WORKSPACE}:/build \ -e EXCLUDE_PATTERNS="/build/doc/exclude-patterns.inc" \ From fbee252354dbfcc977cb182dae314fb4ccdd9fe7 Mon Sep 17 00:00:00 2001 From: Brennan Magee Date: Thu, 29 Jun 2023 11:33:07 +0100 Subject: [PATCH 176/306] fix exclude pattersn --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 266c9d22..4d2e3405 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -42,7 +42,7 @@ pipeline { sh """docker run --user "\$(id -u):\$(id -g)" \ --rm \ -v ${WORKSPACE}:/build \ - -e EXCLUDE_PATTERNS="/build/doc/exclude-patterns.inc" \ + -e EXCLUDE_PATTERNS="/build/doc/exclude_patterns.inc" \ -e PDF=1 \ ghcr.io/xmos/doc_builder:v3.0.0""" archiveArtifacts artifacts: "doc/_build/**", allowEmptyArchive: true From 937e9e2971dad12f7ccb2842d116e180a4264b38 Mon Sep 17 00:00:00 2001 From: Brennan Magee Date: Thu, 29 Jun 2023 11:56:07 +0100 Subject: [PATCH 177/306] Move index.rst to top level and exclude modules This will enable PDF builds --- doc/exclude_patterns.inc | 1 + doc/index.rst => index.rst | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) rename doc/index.rst => index.rst (51%) diff --git a/doc/exclude_patterns.inc b/doc/exclude_patterns.inc index 49de97d5..c798e2c5 100644 --- a/doc/exclude_patterns.inc +++ b/doc/exclude_patterns.inc @@ -1,2 +1,3 @@ # The following patterns are to be excluded from the documentation build tests +modules diff --git a/doc/index.rst b/index.rst similarity index 51% rename from doc/index.rst rename to index.rst index 3f808c15..b026aee8 100644 --- a/doc/index.rst +++ b/index.rst @@ -7,7 +7,7 @@ Framework of camera processing libraries for XCORE.AI. .. toctree:: :maxdepth: 1 - ./1_programming_guide/index - ./2_quick_start_guide/index - ./3_user_guide/index - ./4_release_notes/index + ./doc/1_programming_guide/index + ./doc/2_quick_start_guide/index + ./doc/3_user_guide/index + ./doc/4_release_notes/index From c834a52638815b827242ed995fe38c21c8ce93fc Mon Sep 17 00:00:00 2001 From: Brennan Magee Date: Thu, 29 Jun 2023 11:57:39 +0100 Subject: [PATCH 178/306] Disable pdf builds until svg support added to doc_builder https://github.com/sphinx-doc/sphinx/issues/1907 Alternatively, pre-convert svg to png --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 4d2e3405..4a906837 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -43,7 +43,7 @@ pipeline { --rm \ -v ${WORKSPACE}:/build \ -e EXCLUDE_PATTERNS="/build/doc/exclude_patterns.inc" \ - -e PDF=1 \ + -e PDF= # DISABLED UNTIL SVG SUPPORT ADDED TO DOC_BUILDER\ ghcr.io/xmos/doc_builder:v3.0.0""" archiveArtifacts artifacts: "doc/_build/**", allowEmptyArchive: true } From 8e8c74970a169405c92f75108d33af1fdd09913a Mon Sep 17 00:00:00 2001 From: Brennan Magee Date: Thu, 29 Jun 2023 12:00:55 +0100 Subject: [PATCH 179/306] Fix commenting out syntax of pdf builds --- Jenkinsfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 4a906837..7775513c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -43,8 +43,9 @@ pipeline { --rm \ -v ${WORKSPACE}:/build \ -e EXCLUDE_PATTERNS="/build/doc/exclude_patterns.inc" \ - -e PDF= # DISABLED UNTIL SVG SUPPORT ADDED TO DOC_BUILDER\ ghcr.io/xmos/doc_builder:v3.0.0""" + // DISABLED UNTIL SVG SUPPORT ADDED TO DOC_BUILDER + // -e PDF=1 archiveArtifacts artifacts: "doc/_build/**", allowEmptyArchive: true } } // Build Docs From 2a6f3b7480ebecd476e0f24f3fe16bd7aa382e30 Mon Sep 17 00:00:00 2001 From: Brennan Magee Date: Thu, 29 Jun 2023 12:04:14 +0100 Subject: [PATCH 180/306] parallelise doc build --- Jenkinsfile | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 7775513c..061638fe 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -17,10 +17,10 @@ pipeline { ) } // parameters - stages { + parallel { stage ('Build and Unit test') { agent { - label 'linux&&x86_64&&docker' + label 'linux&&x86_64' } stages { @@ -36,9 +36,32 @@ pipeline { } } } // Build - + + stage('Unit tests') { + steps { + dir('build/tests/unit_tests') { + withTools(params.TOOLS_VERSION) { + sh 'xsim --xscope "-offline trace.xmt" test_camera.xe' + } + } + } + } // Unit tests + + } // stages + post { + cleanup { + cleanWs() + } + } + } // Build Documentation + stage ('Build Documentation') { + agent { + label 'docker' + } + stages { stage ('Build Docs') { steps { + runningOn(env.NODE_NAME) sh """docker run --user "\$(id -u):\$(id -g)" \ --rm \ -v ${WORKSPACE}:/build \ @@ -49,23 +72,12 @@ pipeline { archiveArtifacts artifacts: "doc/_build/**", allowEmptyArchive: true } } // Build Docs - - stage('Unit tests') { - steps { - dir('build/tests/unit_tests') { - withTools(params.TOOLS_VERSION) { - sh 'xsim --xscope "-offline trace.xmt" test_camera.xe' - } - } - } - } // Unit tests - } // stages post { cleanup { cleanWs() } } - } // Build and Unit test + } // Build Documentation } // stages } // pipeline From 0ebef74f4e2fdd3852cb02ff17cc756189758b24 Mon Sep 17 00:00:00 2001 From: Brennan Magee Date: Thu, 29 Jun 2023 12:08:28 +0100 Subject: [PATCH 181/306] fix parallel syntax --- Jenkinsfile | 114 +++++++++++++++++++++++++++------------------------- 1 file changed, 59 insertions(+), 55 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 061638fe..82872a35 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -17,67 +17,71 @@ pipeline { ) } // parameters - parallel { - stage ('Build and Unit test') { - agent { - label 'linux&&x86_64' - } - stages { - - stage ('Build') { - steps { - runningOn(env.NODE_NAME) - // fetch submodules - sh 'git submodule update --init --recursive --jobs 4' - // build examples and tests - withTools(params.TOOLS_VERSION) { - sh 'cmake -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake' - sh 'make -C build -j4' - } + stages { + stage('Builds') { + parallel { + stage ('Build and Unit test') { + agent { + label 'linux&&x86_64' } - } // Build + stages { + stage ('Build') { + steps { + runningOn(env.NODE_NAME) + // fetch submodules + sh 'git submodule update --init --recursive --jobs 4' + // build examples and tests + withTools(params.TOOLS_VERSION) { + sh 'cmake -B build --toolchain=xmos_cmake_toolchain/xs3a.cmake' + sh 'make -C build -j4' + } + } + } // Build - stage('Unit tests') { - steps { - dir('build/tests/unit_tests') { - withTools(params.TOOLS_VERSION) { - sh 'xsim --xscope "-offline trace.xmt" test_camera.xe' + stage('Unit tests') { + steps { + dir('build/tests/unit_tests') { + withTools(params.TOOLS_VERSION) { + sh 'xsim --xscope "-offline trace.xmt" test_camera.xe' + } + } } + } // Unit tests + + } // stages + post { + cleanup { + cleanWs() } } - } // Unit tests + } // Build and Unit test - } // stages - post { - cleanup { - cleanWs() - } - } - } // Build Documentation - stage ('Build Documentation') { - agent { - label 'docker' - } - stages { - stage ('Build Docs') { - steps { - runningOn(env.NODE_NAME) - sh """docker run --user "\$(id -u):\$(id -g)" \ - --rm \ - -v ${WORKSPACE}:/build \ - -e EXCLUDE_PATTERNS="/build/doc/exclude_patterns.inc" \ - ghcr.io/xmos/doc_builder:v3.0.0""" - // DISABLED UNTIL SVG SUPPORT ADDED TO DOC_BUILDER - // -e PDF=1 - archiveArtifacts artifacts: "doc/_build/**", allowEmptyArchive: true + stage ('Build Documentation') { + agent { + label 'docker' + } + stages { + stage ('Build Docs') { + steps { + runningOn(env.NODE_NAME) + sh """docker run --user "\$(id -u):\$(id -g)" \ + --rm \ + -v ${WORKSPACE}:/build \ + -e EXCLUDE_PATTERNS="/build/doc/exclude_patterns.inc" \ + ghcr.io/xmos/doc_builder:v3.0.0""" + // DISABLED UNTIL SVG SUPPORT ADDED TO DOC_BUILDER + // -e PDF=1 + archiveArtifacts artifacts: "doc/_build/**", allowEmptyArchive: true + } + } // Build Docs + } // stages + post { + cleanup { + cleanWs() + } } - } // Build Docs - } // stages - post { - cleanup { - cleanWs() - } - } - } // Build Documentation + } // Build Documentation + } // parallel + } //Builds } // stages } // pipeline From 191008253daf1d6c1e171db979c2bc7df3e4579c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Thu, 29 Jun 2023 12:25:50 +0100 Subject: [PATCH 182/306] minor doc correction --- .../02_Architecture_and_Design.rst | 4 +-- doc/2_quick_start_guide/Quick_Start_guide.rst | 29 ++++++++++++------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/doc/1_programming_guide/02_Architecture_and_Design.rst b/doc/1_programming_guide/02_Architecture_and_Design.rst index 685c2d60..6e5ff52b 100644 --- a/doc/1_programming_guide/02_Architecture_and_Design.rst +++ b/doc/1_programming_guide/02_Architecture_and_Design.rst @@ -16,13 +16,13 @@ From a Hardware point of view, fwk_camera is composed of the following modules: #. Camera connector : 15-PIN MIPI CSI connector for the camera module. (compatible with Raspberry Pi). #. Mipi Shim : Xmos MIPI hardware to convert MIPI signals to Xcore ports. #. Xcore : Xmos Xcore processor to process the MIPI signals. -#. Xtag Host connector : USB connector to connect to the host. +#. xTag Host connector : USB connector to connect to the host. Software Architecture ------------------------- From a Software point of view, fwk_camera is composed of the following modules: -.. figure:: images/2_object_diagram.svg +.. figure:: images/2_object_diagram.png :alt: fwk_camera object diagram :figclass: custom-class diff --git a/doc/2_quick_start_guide/Quick_Start_guide.rst b/doc/2_quick_start_guide/Quick_Start_guide.rst index a822661f..48ae2efd 100644 --- a/doc/2_quick_start_guide/Quick_Start_guide.rst +++ b/doc/2_quick_start_guide/Quick_Start_guide.rst @@ -10,8 +10,8 @@ Hardware requirements: - XCORE.AI EVALUATION KIT (XK-EVK-XU316) - Camera module - Camera ribbon connector -- 2xMicro USB cable (Power supply and Xtag) -- Xtag debugger and cable +- 2xMicro USB cable (Power supply and xTag) +- xTag debugger and cable Software requirements: ^^^^^^^^^^^^^^^^^^^^^^^ @@ -30,12 +30,18 @@ Run the RAW camera demo This demo uses the RAW camera module to capture a RAW8 image and save it to a .raw file. Then, this image can be decoded using the python script ``python decode_raw8.py``. -.. note:: - Make sure xscope_fileio is installed. See /utils/README.rst section for more details +.. warning:: + Make sure xscope_fileio is installed. See utils/README.rst section for more details -#. Ensure that the camera is connected to the board -#. Connect Power Supply and Xtag debugger -#. Build the example using the following commands: +1. Ensure that the camera is connected to the board +2. Connect Power Supply and xTag debugger +3. Install python requirements. From the root of the repository run: + +.. code-block:: console + + pip install -r requirements.txt + +4. Build the example using the following commands: .. tab:: Linux and Mac @@ -52,17 +58,18 @@ Then, this image can be decoded using the python script ``python decode_raw8.py` ninja -C build example_take_picture_raw -4. Run the example using the following command: +5. Run the example using the following command: .. code-block:: console python python/run_xscope_bin.py build/examples/take_picture_raw/example_take_picture_raw.xe -5. You should see the camera comminucating with the host and the image being saved to a .raw file -6. To decode the image use the following command: +6. You should see the camera comminucating with the host and the image being saved to a .raw file. + +7. To decode the image use the following command: .. code-block:: console python python/decode_raw8.py -7. You should see the decoded image displayed on the screen +8. You should see the decoded image displayed on the screen From ba50fa4b26e34c6a1a443a91309168cc2fad0566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Thu, 29 Jun 2023 14:52:34 +0100 Subject: [PATCH 183/306] adding .pdf --- Jenkinsfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 82872a35..13ab5aea 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -68,9 +68,8 @@ pipeline { --rm \ -v ${WORKSPACE}:/build \ -e EXCLUDE_PATTERNS="/build/doc/exclude_patterns.inc" \ - ghcr.io/xmos/doc_builder:v3.0.0""" - // DISABLED UNTIL SVG SUPPORT ADDED TO DOC_BUILDER - // -e PDF=1 + -e PDF=1 \ + ghcr.io/xmos/doc_builder:v3.0.0""" archiveArtifacts artifacts: "doc/_build/**", allowEmptyArchive: true } } // Build Docs From 6582da369e0d08b33920772ce4e7840848f0b0ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Thu, 29 Jun 2023 15:06:50 +0100 Subject: [PATCH 184/306] testing removing .png --- doc/1_programming_guide/01_Introduction.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/doc/1_programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst index da376b1f..955521bc 100644 --- a/doc/1_programming_guide/01_Introduction.rst +++ b/doc/1_programming_guide/01_Introduction.rst @@ -37,11 +37,6 @@ The FWK_Camera alongside with the Explorer board architecture provides the follo This repository contains a set of tools for image acquisition, processing, and transmission. The architecture, viewed from a high level, is composed of the following elements: -.. figure:: images/1_high_level_view.png - :alt: High-level block diagram - :align: center - - High-level block diagram of the FWK_Camera. 1. Camera hardware and interface 2. Camera drivers From f4db52989667b5da55cb3a81d1cbb5924eef5a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Thu, 29 Jun 2023 15:19:33 +0100 Subject: [PATCH 185/306] fixing .pdf build error --- doc/1_programming_guide/01_Introduction.rst | 11 ++++++++--- doc/exclude_patterns.inc | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/1_programming_guide/01_Introduction.rst b/doc/1_programming_guide/01_Introduction.rst index 955521bc..2a407b75 100644 --- a/doc/1_programming_guide/01_Introduction.rst +++ b/doc/1_programming_guide/01_Introduction.rst @@ -32,11 +32,16 @@ The FWK_Camera alongside with the Explorer board architecture provides the follo - Low-resolution filtering - Supported cameras: - IMX219 - - GC2145 [*]_ + - GC2145 [0]_ This repository contains a set of tools for image acquisition, processing, and transmission. The architecture, viewed from a high level, is composed of the following elements: +.. figure:: images/1_high_level_view.png + :alt: High-level block diagram + :align: center + + High-level block diagram of the FWK_Camera. 1. Camera hardware and interface 2. Camera drivers @@ -51,7 +56,7 @@ Getting Started To start using the FWK_Camera, you can proceed to the Quick Start Guide, go to: - :ref:`QS_FWKC`. +:ref:`QS_FWKC`. Additional Resources --------------------- @@ -62,4 +67,4 @@ Additional Resources - IMX219 datasheet: `IMX219`_ -.. [*] With Hardware modifications. +.. [0] With Hardware modifications. diff --git a/doc/exclude_patterns.inc b/doc/exclude_patterns.inc index c798e2c5..cd9fbfb8 100644 --- a/doc/exclude_patterns.inc +++ b/doc/exclude_patterns.inc @@ -1,3 +1,4 @@ # The following patterns are to be excluded from the documentation build tests modules +sensors From bc716157e0fa66f90f0992e59a0e674d07199072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Fri, 30 Jun 2023 15:38:40 +0100 Subject: [PATCH 186/306] asserting apps --- examples/take_picture_downsample/src/app.c | 13 +++++++------ examples/take_picture_raw/src/app_raw.c | 7 +++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index ed7955f8..f9171cad 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -1,6 +1,9 @@ +// std +#include +#include #include #include -#include +#include #include "io_utils.h" #include "app.h" @@ -19,12 +22,10 @@ void user_app() // Wait for the image to set exposure delay_milliseconds(4000); - printf("Requesting image...\n"); + // grab a frame - if(camera_capture_image(image_buffer)){ - printf("Error capturing image\n"); - exit(1); - } + printf("Requesting image...\n"); + assert(camera_capture_image_raw(image_buffer) == 1); printf("Image captured...\n"); // stop the threads and camera stream diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index 28a89967..22a5048a 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -1,5 +1,7 @@ // std +#include #include +#include // xcore #include #include @@ -23,10 +25,7 @@ void user_app_raw() // Request an image printf("Requesting image...\n"); - if(camera_capture_image_raw(image_buffer)){ - printf("Error capturing image\n"); - exit(1); - } + assert(camera_capture_image_raw(image_buffer) == 1); printf("Image captured...\n"); // stop the threads and camera stream From 0b458a38fa7facc67ecf703f7fef05da5c867fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Fri, 30 Jun 2023 15:39:04 +0100 Subject: [PATCH 187/306] cmake comment --- examples/take_picture_downsample/CMakeLists.txt | 2 +- examples/take_picture_raw/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/take_picture_downsample/CMakeLists.txt b/examples/take_picture_downsample/CMakeLists.txt index cd566588..53bb4b45 100644 --- a/examples/take_picture_downsample/CMakeLists.txt +++ b/examples/take_picture_downsample/CMakeLists.txt @@ -1,4 +1,4 @@ -# <--- Set the executable name +# Executable name set(TARGET example_take_picture_downsample) #********************** diff --git a/examples/take_picture_raw/CMakeLists.txt b/examples/take_picture_raw/CMakeLists.txt index 9ea7118e..01dd21d6 100644 --- a/examples/take_picture_raw/CMakeLists.txt +++ b/examples/take_picture_raw/CMakeLists.txt @@ -1,4 +1,4 @@ -# <--- Set the executable name +# Executable name set(TARGET example_take_picture_raw) #********************** From 3bda59d2ce6e3a6891499f3ceab94e68ae56a6f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 3 Jul 2023 11:45:26 +0100 Subject: [PATCH 188/306] internally generated working example --- camera/api/MipiPacketRx_simulate.h | 17 ++++++ camera/src/MipiPacketRx_simulate.c | 49 +++++++++++++++ camera/src/camera_main.xc | 12 +++- examples/CMakeLists.txt | 2 + examples/take_picture__local/CMakeLists.txt | 17 ++++++ examples/take_picture__local/src/app.c | 67 +++++++++++++++++++++ examples/take_picture__local/src/app.h | 7 +++ examples/take_picture__local/src/main.xc | 51 ++++++++++++++++ examples/take_picture_downsample/src/app.c | 2 +- examples/take_picture_raw/src/app_raw.c | 2 +- 10 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 camera/api/MipiPacketRx_simulate.h create mode 100644 camera/src/MipiPacketRx_simulate.c create mode 100644 examples/take_picture__local/CMakeLists.txt create mode 100644 examples/take_picture__local/src/app.c create mode 100644 examples/take_picture__local/src/app.h create mode 100644 examples/take_picture__local/src/main.xc diff --git a/camera/api/MipiPacketRx_simulate.h b/camera/api/MipiPacketRx_simulate.h new file mode 100644 index 00000000..a5c7ac21 --- /dev/null +++ b/camera/api/MipiPacketRx_simulate.h @@ -0,0 +1,17 @@ +#include "mipi.h" + +#ifdef __XC__ +extern "C" { +#endif + + +void MipiPacketRx_simulate( + in_buffered_port_32_t p_mipi_rxd, + in_port_t p_mipi_rxa, + streaming_chanend_t c_pkt, + streaming_chanend_t c_ctrl); + + +#ifdef __XC__ +} +#endif diff --git a/camera/src/MipiPacketRx_simulate.c b/camera/src/MipiPacketRx_simulate.c new file mode 100644 index 00000000..1ba9cebf --- /dev/null +++ b/camera/src/MipiPacketRx_simulate.c @@ -0,0 +1,49 @@ +#include +#include + +#include "packet_handler.h" + +#include "MipiPacketRx_simulate.h" + +#define STEP_JUMP 1 + +void MipiPacketRx_simulate( + in_buffered_port_32_t p_mipi_rxd, + in_port_t p_mipi_rxa, + streaming_chanend_t c_pkt, + streaming_chanend_t c_ctrl) +{ + while (1) { + // send data + for (int j = 0; j < MIPI_IMAGE_HEIGHT_PIXELS + 2; j++) { + mipi_packet_t* pkt = (mipi_packet_t*)s_chan_in_word(c_pkt); + // if null pointer return + if (pkt == NULL) { + return; + } + // else continue + switch (j) + { + case 0: + pkt->header = (uint32_t)MIPI_DT_FRAME_START; + break; + + case MIPI_IMAGE_HEIGHT_PIXELS + 1: + pkt->header = (uint32_t)MIPI_DT_FRAME_END; + break; + + default: + pkt->header = (uint32_t)MIPI_DT_RAW8; // header type RAW8 + for (int i = 0; i < MIPI_MAX_PKT_SIZE_BYTES; i = i + STEP_JUMP) { + int8_t pixel_value = (i % 256) - 128; + pkt->payload[i] = pixel_value; + } + break; + } + // send back the data + s_chan_out_word(c_pkt, (unsigned)pkt); + delay_milliseconds(2); // LB + } + delay_milliseconds(10); // FB + } +} diff --git a/camera/src/camera_main.xc b/camera/src/camera_main.xc index 74ee1572..9d67d85c 100644 --- a/camera/src/camera_main.xc +++ b/camera/src/camera_main.xc @@ -14,6 +14,16 @@ #include "isp.h" #include "sensor_control.h" +#define SIMULATION 1 + +#if (SIMULATION == 1) + #include "MipiPacketRx_simulate.h" + #define MipiPacketRx_function(...) MipiPacketRx_simulate(__VA_ARGS__) +#else + #define MipiPacketRx_function(...) MipiPacketRx(__VA_ARGS__) +#endif + + void camera_main( tileref mipi_tile, in port p_mipi_clk, @@ -58,7 +68,7 @@ void camera_main( // start the different jobs (packet controller, handler, and post_process) par { - MipiPacketRx(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); + MipiPacketRx_function(p_mipi_rxd, p_mipi_rxa, c_pkt, c_ctrl); mipi_packet_handler(c_pkt, c_ctrl, c_stat_thread); isp_pipeline(c_stat_thread, sc_if); sensor_control(sc_if, i2c); diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index d7cb55b9..13058b7d 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -36,3 +36,5 @@ if(${CONFIG_MIPI_FORMAT} STREQUAL ${CONFIG_RAW8}) # we only support RAW8 images in this example for now add_subdirectory(take_picture_downsample) endif() + +add_subdirectory(take_picture__local) diff --git a/examples/take_picture__local/CMakeLists.txt b/examples/take_picture__local/CMakeLists.txt new file mode 100644 index 00000000..68b05456 --- /dev/null +++ b/examples/take_picture__local/CMakeLists.txt @@ -0,0 +1,17 @@ +# Executable name +set(TARGET example_take_picture_local) + +#********************** +# Targets +#********************** +add_executable(${TARGET}) +target_sources(${TARGET} + PRIVATE + src/app.c + src/main.xc +) + +target_include_directories(${TARGET} PUBLIC src) +target_compile_options(${TARGET} PRIVATE ${EXAMPLE_APP_COMPILER_FLAGS}) +target_link_libraries(${TARGET} PUBLIC ${EXAMPLE_APP_LINK_LIBRARIES}) +target_link_options(${TARGET} PRIVATE ${EXAMPLE_APP_LINK_OPTIONS}) diff --git a/examples/take_picture__local/src/app.c b/examples/take_picture__local/src/app.c new file mode 100644 index 00000000..9832282d --- /dev/null +++ b/examples/take_picture__local/src/app.c @@ -0,0 +1,67 @@ +// std +#include +#include +#include +#include +#include + +#include "io_utils.h" +#include "app.h" +#include "isp.h" // needed for gamma + +void user_app() +{ + // Initialize camera api + camera_init(); + + int8_t image_buffer[APP_IMAGE_HEIGHT_PIXELS][APP_IMAGE_WIDTH_PIXELS][APP_IMAGE_CHANNEL_COUNT]; + uint8_t * image_ptr = (uint8_t *) &image_buffer[0][0][0]; + + // set the input image to 0 + memset(image_buffer, -128, sizeof(image_buffer)); + + // Wait for the image to set exposure + delay_milliseconds(4000); + + // grab a frame + printf("Requesting image...\n"); + assert(camera_capture_image(image_buffer) == 0); + printf("Image captured...\n"); + + // stop the threads and camera stream + camera_stop(); + delay_milliseconds(100); + + // convert to uint8 + vect_int8_to_uint8(image_ptr, + &image_buffer[0][0][0], + sizeof(image_buffer)); + + // apply gamma correction + #if APPLY_GAMMA + isp_gamma(image_ptr, + &gamma_new[0], + APP_IMAGE_HEIGHT_PIXELS, + APP_IMAGE_WIDTH_PIXELS, + APP_IMAGE_CHANNEL_COUNT); + #endif + + // Write binary file + write_image_file("capture.bin", + image_ptr, + APP_IMAGE_HEIGHT_PIXELS, + APP_IMAGE_WIDTH_PIXELS, + APP_IMAGE_CHANNEL_COUNT); + + // Write bmp file + write_bmp_file("capture.bmp", + image_ptr, + APP_IMAGE_HEIGHT_PIXELS, + APP_IMAGE_WIDTH_PIXELS, + APP_IMAGE_CHANNEL_COUNT); + + printf("Images saved. Exiting.\n"); + xscope_close_all_files(); + // end here + exit(0); +} diff --git a/examples/take_picture__local/src/app.h b/examples/take_picture__local/src/app.h new file mode 100644 index 00000000..1ddfd014 --- /dev/null +++ b/examples/take_picture__local/src/app.h @@ -0,0 +1,7 @@ +#include "xs1.h" +#include "platform.h" +#include "xccompat.h" + +#include "camera_main.h" + +void user_app(); diff --git a/examples/take_picture__local/src/main.xc b/examples/take_picture__local/src/main.xc new file mode 100644 index 00000000..cf73be13 --- /dev/null +++ b/examples/take_picture__local/src/main.xc @@ -0,0 +1,51 @@ +#include +#include +#include + +#include "print.h" +#include "i2c.h" +#include "camera_main.h" +#include "app.h" + +// I2C interface ports +#define Kbps 400 +on tile[0]: port p_scl = XS1_PORT_1N; +on tile[0]: port p_sda = XS1_PORT_1O; + +extern "C" { +#include "xscope_io_device.h" +} + +/** +* Declaration of the MIPI interface ports: +* Clock, receiver active, receiver data valid, and receiver data +*/ +on tile[MIPI_TILE] : in port p_mipi_clk = XS1_PORT_1O; +on tile[MIPI_TILE] : in port p_mipi_rxa = XS1_PORT_1E; // activate +on tile[MIPI_TILE] : in port p_mipi_rxv = XS1_PORT_1I; // valid +on tile[MIPI_TILE] : buffered in port:32 p_mipi_rxd = XS1_PORT_8A; // data +on tile[MIPI_TILE] : clock clk_mipi = MIPI_CLKBLK; + + +int main(void) +{ + i2c_master_if i2c[1]; + chan xscope_chan; + par { + xscope_host_data(xscope_chan); + // printf("im here\n"); + on tile[0]: i2c_master(i2c, 1, p_scl, p_sda, Kbps); + + on tile[MIPI_TILE]: camera_main(tile[MIPI_TILE], + p_mipi_clk, + p_mipi_rxa, + p_mipi_rxv, + p_mipi_rxd, + clk_mipi, + i2c[0]); + + on tile[MIPI_TILE]: xscope_io_init(xscope_chan); + on tile[MIPI_TILE]: user_app(); + } + return 0; +} diff --git a/examples/take_picture_downsample/src/app.c b/examples/take_picture_downsample/src/app.c index f9171cad..9832282d 100644 --- a/examples/take_picture_downsample/src/app.c +++ b/examples/take_picture_downsample/src/app.c @@ -25,7 +25,7 @@ void user_app() // grab a frame printf("Requesting image...\n"); - assert(camera_capture_image_raw(image_buffer) == 1); + assert(camera_capture_image(image_buffer) == 0); printf("Image captured...\n"); // stop the threads and camera stream diff --git a/examples/take_picture_raw/src/app_raw.c b/examples/take_picture_raw/src/app_raw.c index 22a5048a..f246f29f 100644 --- a/examples/take_picture_raw/src/app_raw.c +++ b/examples/take_picture_raw/src/app_raw.c @@ -25,7 +25,7 @@ void user_app_raw() // Request an image printf("Requesting image...\n"); - assert(camera_capture_image_raw(image_buffer) == 1); + assert(camera_capture_image_raw(image_buffer) == 0); printf("Image captured...\n"); // stop the threads and camera stream From de04bc4219eec4fbf7c5c388371133808e9f8fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xalbertoisorna=E2=80=9D?= Date: Mon, 3 Jul 2023 17:03:48 +0100 Subject: [PATCH 189/306] reverse conversion using xscope fileio ok --- camera/CMakeLists.txt | 7 +++- camera/src/MipiPacketRx_simulate.c | 42 ++++++++++++++++--- camera/src/camera_main.xc | 4 +- python/{filters.txt => _filters.txt} | 0 python/encode_raw8.py | 60 +++++++++++++++++++++++++++ python/test_imgs/test.jpg | Bin 0 -> 406185 bytes 6 files changed, 104 insertions(+), 9 deletions(-) rename python/{filters.txt => _filters.txt} (100%) create mode 100644 python/encode_raw8.py create mode 100644 python/test_imgs/test.jpg diff --git a/camera/CMakeLists.txt b/camera/CMakeLists.txt index affea754..53d94d1f 100644 --- a/camera/CMakeLists.txt +++ b/camera/CMakeLists.txt @@ -36,5 +36,10 @@ target_compile_options(${LIB_NAME} -fxscope -mcmodel=large ) -target_link_libraries(${LIB_NAME} PUBLIC mipi::lib_mipi sensors::lib_imx) +target_link_libraries(${LIB_NAME} PUBLIC + mipi::lib_mipi + sensors::lib_imx + fwk_camera::utils + fwk_camera::xscope_fileio) + add_library(camera::lib_camera ALIAS ${LIB_NAME}) diff --git a/camera/src/MipiPacketRx_simulate.c b/camera/src/MipiPacketRx_simulate.c index 1ba9cebf..7371a11f 100644 --- a/camera/src/MipiPacketRx_simulate.c +++ b/camera/src/MipiPacketRx_simulate.c @@ -1,18 +1,49 @@ #include #include -#include "packet_handler.h" - #include "MipiPacketRx_simulate.h" +#include "packet_handler.h" +#include "io_utils.h" +#include + #define STEP_JUMP 1 +//function to fill the array +/* +static +void fill_array(mipi_packet_t* pkt) { + int8_t pixel_value = 0; + for (int i = 0; i < MIPI_MAX_PKT_SIZE_BYTES; i = i + STEP_JUMP) { + if (i % 2) { + pixel_value = (i % 256) - 128; + } + else { + pixel_value = -128; + } + pkt->payload[i] = pixel_value; + } +} +*/ + +static +void fill_array_from_file(xscope_file_t *fp, mipi_packet_t* pkt) { + xscope_fread( + fp, + (uint8_t*) &pkt->payload[0], + MIPI_IMAGE_WIDTH_BYTES); + //printf("num_bytes: %d\n", num_bytes); +} + void MipiPacketRx_simulate( in_buffered_port_32_t p_mipi_rxd, in_port_t p_mipi_rxa, streaming_chanend_t c_pkt, streaming_chanend_t c_ctrl) { + const char * filename = "test_generated_int8.raw"; + xscope_file_t fp = xscope_open_file(filename, "rb"); + while (1) { // send data for (int j = 0; j < MIPI_IMAGE_HEIGHT_PIXELS + 2; j++) { @@ -34,16 +65,15 @@ void MipiPacketRx_simulate( default: pkt->header = (uint32_t)MIPI_DT_RAW8; // header type RAW8 - for (int i = 0; i < MIPI_MAX_PKT_SIZE_BYTES; i = i + STEP_JUMP) { - int8_t pixel_value = (i % 256) - 128; - pkt->payload[i] = pixel_value; - } + // fill_array(pkt); + fill_array_from_file(&fp, pkt); break; } // send back the data s_chan_out_word(c_pkt, (unsigned)pkt); delay_milliseconds(2); // LB } + xscope_fseek(&fp, 0, SEEK_SET); delay_milliseconds(10); // FB } } diff --git a/camera/src/camera_main.xc b/camera/src/camera_main.xc index 9d67d85c..5530e467 100644 --- a/camera/src/camera_main.xc +++ b/camera/src/camera_main.xc @@ -13,11 +13,11 @@ #include "packet_handler.h" #include "isp.h" #include "sensor_control.h" +#include "MipiPacketRx_simulate.h" #define SIMULATION 1 -#if (SIMULATION == 1) - #include "MipiPacketRx_simulate.h" +#if (SIMULATION == 1) #define MipiPacketRx_function(...) MipiPacketRx_simulate(__VA_ARGS__) #else #define MipiPacketRx_function(...) MipiPacketRx(__VA_ARGS__) diff --git a/python/filters.txt b/python/_filters.txt similarity index 100% rename from python/filters.txt rename to python/_filters.txt diff --git a/python/encode_raw8.py b/python/encode_raw8.py new file mode 100644 index 00000000..0813666a --- /dev/null +++ b/python/encode_raw8.py @@ -0,0 +1,60 @@ +# load a png or jpg image and encodes to RAW8 format +import os +import cv2 +import matplotlib.pyplot as plt +import numpy as np +from PIL import Image # just to avoid color BGR issues when writting +import sys + +# append current path to sys.path +from pathlib import Path +root = (str(Path(__file__).resolve().parent)) + +# reads an image using pillow +img = Image.open(root+"/test_imgs/test.jpg") +img = img.convert('RGB') +img = img.resize((640//2, 480//2)) + +# convert to numpy array +img = np.array(img).astype(np.uint8) +raw_img = np.zeros((480, 640), dtype=np.uint8) + +# split in color planes +raw_img[0::2, 0::2] = img[:,:,0] +raw_img[0::2, 1::2] = img[:,:,1] +raw_img[1::2, 0::2] = img[:,:,1] +raw_img[1::2, 1::2] = img[:,:,2] +raw_img = raw_img.clip(0, 255).astype(np.uint8) + +# 1 gamma +raw_img = raw_img/255.0 +raw_img = np.power(raw_img, 1.8) +raw_img = raw_img*255.0 +raw_img = raw_img.clip(0, 255).astype(np.uint8) + +# 2 awb +rgb_gain = [0.5, 0.9, 0.5] +raw_img[0::2, 0::2] = raw_img[0::2, 0::2]*rgb_gain[0] +raw_img[0::2, 1::2] = raw_img[0::2, 1::2]*rgb_gain[1] +raw_img[1::2, 0::2] = raw_img[1::2, 0::2]*rgb_gain[1] +raw_img[1::2, 1::2] = raw_img[1::2, 1::2]*rgb_gain[2] +raw_img = raw_img.clip(0, 255).astype(np.uint8) + +# 3 black level substraction +BLACK_LEVEL = 16 +raw_img += BLACK_LEVEL +raw_img = raw_img.astype(np.uint8) + +# save as a binary file +name = r"/Users/albertoisorna/exec/fwk_camera_windows/test_generated.raw" +with open(name, "wb") as img: + img.write(raw_img) + +# save as int8 +raw_img = raw_img.astype(np.int16) +raw_img -= 128 +raw_img = raw_img.astype(np.int8) +name = r"/Users/albertoisorna/exec/fwk_camera_windows/test_generated_int8.raw" +with open(name, "wb") as img: + img.write(raw_img) + diff --git a/python/test_imgs/test.jpg b/python/test_imgs/test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dd6df8dfdf6254ef68b956f3c2a34a3fae8b6260 GIT binary patch literal 406185 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

`1JWfHCg&7# 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®dSK4wj1tgz%1?46}T(ZAt4gkuI zCQGO~{3!;GP%5obC_#aw0nB%fNU=<~vbyNg3i-NJa0MYxKV*tFm55Dn(VK zMYdBM%tKrrb~YhX$nbQ!ckRYM=sC6wF%HAiNaBc|=HXz0&1f)9WkcBnqdN3VeQK=mWq?FRTq-7) z*Y+UY;^U36)%FIYLT&kI5W>ne%kdX$U7;!ww&ZkbkyUWr$*+?5$%*4awE6bXAjGqy zg}>JdBja1tEJ$B4O-Av#{#+8Our1&U)J+-XQ?%tb4o@Q@HpL^)Fx+wuO)R*cA2!e1 zM9&yZUwlx%gQuRD+;IjNozCx1-7c0P>C#Jc$XRpn49WHSl**hSv4C0mswKvDw*sol zaOiROY(p0Yi~gulP51-}mo~VE(;%~m#*c-h4@``-;#=1mW~)ed+3q=9T`obj=nzi~ zykt$qi04{i-X}9}tq`umL|6EWcOBV{cwWXkN@iq4BdcTRRbko z_k4GP_t`(#*!8KZ@zm~c9>Zw@U#Iqd{rpL0T7EV8P3gJlB8cSh-zj!s>*0~h2Fpm? zVJ=07sv{BAR6&lo#!GU;M%h@dA{^&P2W98VZmo!#-JdGtPMRHYB7&pEaoYQ73<(__ zA+)TnSy52`Sb6Cajjhd6I_8g^1kqx(yP*CW-%M5AS>Pp_)cp5u?ViGkmv{g2HkW-G z24r5_iin6>KRnS_gX^nn$pmITQ|4X3wAzScI1;%qs~u%zTw67kn|$Dl#@0!qOzlca?QaX=gg`GWa@sAfk|JZG2; zn6-7}alndFbVpogv9)h{Qyr?)#RdaC%qCUEL%*qVnZn~tjq*|4cvFYW)vB{`LFTAS z;Ky5_#JB^XDkn{nTLEg!PEVhsb9sWYR*&cM?Fa8c4vdt*Vvd^g0wN-oyVvAOTrtC} zvWaM$bzl|8rEkOb!mPo}8KOYiSq1#YoLz$=x#+={9fx z+dd~jh_W=J$|J{XfzAYmD?cH@g#Og!79~C{+;SE9^LxtO zS_fn8%*ywAQIUx$oP&VV)#D6vBh{BCT%HQ?yct_d6vdsGe71?xC+R~zLNX1TH>JB; zGrNwC3n!k|Lpc=U_Ni5#_IAsOO0bU(N@;SXLnfS4a~v!m6WORU4FVEt)Z|CFG|-zn zI}S#H044Fz(Z=4f1y4xgtW73!{NdXU02?wqugBS~^*aXmB&v-LW#l!DjNzYifRD`4z5ehZ&n;Md@>%J`cz&=UQOY?N9qw}m|ryyTK zCxmU9OM5v17D9Ffz{zE|1*i}lP>HbXLMR%Fz^g^~&V+WD5u&IwRa--QjJxh-(_Q_<&N5Z_? zIy&od*R5=HlUY6U^)}y=ZmjreRTwFlyDB%%l17@jYwji5MjG?vPcei*wiF6b;nr=T z;oe)%H8jZCehs@%)Tg}&Q)|-62>OflBD@k?To9QW5Woo$q8MII7v}~>IyTKJuV-w( z>=0}aX0{Qhmuk`&+?;V_6&x~~EFAo}cNgP@Tq!WkbWbWQI{85;^a3N7Tsr21D*z2yRs=horkyw#cYkIlF9trPQOc18nm%Cu!`NXeL10jGz zIu76a04|$YqYGxmL8EQSN?Q(tQ1@WwYnj|!nfW8l0NUqg%=w<)XHAfMvC4{(#7@O_ z%b^7KkNSpi@CMl6V)1!~MG3a6fsycHuuuuO7Trf?9F}|Dfd!oF` z!R5bL(=|7Wyq~smjNg0lEOvOJz?WQIpNx#^QWrYB4vd_&>&JD{-zHC#8b5;~9YmD!c?7W!Q z*F3XkLwQkv8RSp0x*esd+U7qI(cnTd40Bz zK&E%%yZEm!VFX_Qnm_9}(2uTm$@i2L7EAVdD|CUO%Nx*YvnV%w(5II>NiTRaIu$tH zQY=mmf1+Ru{b2Z!fd?JrKP97prn^6Vbl6%eq8SN-#1~zuTgWTD!wHuo-V4nUhbh?r*B}ohxkkRJ9Za2~}G?q8rUX>h#k;d7%w!d)8 z-s=mZFWSal$cbo%HM$GKZG%bvaoJCBu(7dmFlYH5Kf(GB>k)uS@ZTvu59WW|HeC4C zn=mqY?GkK|uBXUr%kWg>xMI5sGvSL|a(Zn`FVktT#vKd3SjrCT zbOZ^;w8SgY4_(i%kiMWww)r!}AGh=QzL6H+;p2Tep*Xqvk?({Fo47TMl6XO#7rx23 zfVgZ2>q1HBjePa#mu4C4r(boL zoqSRTq#>)*8hOaXvGjrf5|bL^v`4@*lG(pI2a3R|W;v;SM@-jpSp~D{SZ9=4hUXR{u`wpY+&lq(NCwA_JdDtOPF}oI=(7oyKN~%&?@R{I_^iwth@@X?JI3a;y;Q5jeexLm zpD(upmN;)J(9*k2xq0asUA|C|`eRC1>|}JY+@7Gr7cxBTjalcMGexQv`3Minw;9X` z*Z45LR}1UC6RUF3L#p?sPw)kabrI@b-YYtqj)ZPDlzShj3M)fhY9xRC#bRF+y(@{H z)e?C!%>s}Y=a05>)hJDH4-?9Bx(X*OV^R$WV*UyL=ASHrNnyR(6ti1U{LUuObxt9L zQ(6(kx9Hf1PJTf-OoYx%Yyrm0|7m-Tc$+~C)IwP$E9@qn_Y$TGcU5ii z;-9}g`#efM``KpzBwj+6lPM{VLEe1Q0d!nnrsr}vxvxkPz{@>iK&jebi~ za^Z_4(gqa)0fZ@~dUJ=7r}7-eRQ(7_IRq)G@1_ zt@mPCx!VVA!Sp0<8ugA+g7NicsrM#jk0g7F)XZCprmLoNU055VhSim;1V=8%hPG8V z_p@XaVXVL|^3^(e1Kf!hhAGQWvU9fEZ1UcRo9tp`li8Z$YhQ)Uv`BetYJU2A==;ot zWJHAV)6C|XlU4HlyMjn~{EhdooaiXh5mqt$GDJ{^>kIglUkAgK-jh#o6p{VnCh`Rv z;}_ndsKZY)XnuF3(QV7<&?_G^EtA5hL$CE#N!1P(NOw#1W~suWQ)B<56Xp_`Rnczv z7HD$NT#p~OF7xMPtzrv0XZ)#)2>I*+zEQjr#1_@?FR?dl@p2p0XBWD8#IVm5tl&F}4WO!RrhY)}_TW?EI|U!6y? z{49bmanC;fZgCq8Gm*1T8nF;tV}(OPh;>G|;|CdOM&@LY!|E~D=;F%(>1 z9IEgHcT`1X9{4JDHz;a4^J{cXtl$W%aBF@BbJ+s_H^DPJLi^{gOM2r$1}Fw}v(+zN zahqS3u|s=fDAG`lQ+;Red;&(KA8h2`bmQ^nX%GeQF>sUMy6KilA9yFkdANPEbiHQ+ z*nwsl;%K+Qo6;rSZ(gTt#<2N?n8g=|wKO#{Ov5OeCs(bfLW${3yTa{!?&z~k7zo>V zJBvf?S0v*jUa{gDX0=p(iL~^xE$>V%m==_Nw6H+!*bKcS#j}x1)Cn10k3Bz~`*ORH z6cftUJmDc&oOI1rf`;Hew>KAp)UVEPyya%6Zf#8N92okG<**&&9W%n2WL49LPAYhj zR&he8XK2WbG*&30f-S6>*`#eS$uQ*c{+^IK4m+U0m_L4{lYju=sRW1yJL8%Eyb zIqVo1`=kj<)&Fm|EV3wB3_OhUEkgH56)cGMy@C&K*TYE=>-gncuZ1E)OVF_-1f%4o zd5znNKWd25ZqCP9dx*Ve_R&!vM0drl+Wv#@O0-mExV? zBgkp~#mUpu|HiNitBRLilG-35Aa}nqL0X1(7zytUvgRMFqNh%pAAcir7v7tlW zB*ohl2a`YCU50NIRi1dQ*gZwj2^g9)z71$lUvwqROGqRJI}3d6$K_akdMg>Q)1F%2 zeHd>tDTlyq!|p{mtB?q`5dygQa~Yc--y;lhlR3}?9$2U0G3CEE#6Qr*d?9}ATO(&B z7D})c{=u+_sp-%Li9*MkquB8ms9x@_G0Ztj_u{=MD9;aBuof7Bl1BxHEQA%=^# z)3f!1fG^;Ev3i2%IX$p^+mBRlWq2QcfbdNm`x;E*@UmoVXNU(2XwJ*<_~b+<;GGT* z^9e7_yr=t%{n#Ww?L^m}>Qrp5k@AxhhN>z;c&477WMQoh%EXo%qc+FkzQ}`C&jGF5 zU#!&dwSuXWq{e$W>fxn;BQ)48NLzeR_;!e3;4jJdq#0@IXgok7_)$#K^& zre`(g^KDH7ekUG{0RCcaSw3DWpA$n|#d0PsY_&xwvA4xuBzXvO5#93UGTthWq+;P> z#fwKi%{TCVHtGVAW80W;mMLfhXJXR3R`5|Ysk`*;y-}*$k$fBBXVVbp~2tB2(!ADDhlBnXi z=147a8%~^N9C7{38kjLVNp7FZ{`F7s6LrFV#lI-#5S*bH>+`}Nh-Y*Hlp z6_J^1gl(8dtYKtXmkK^v!lQU~`6YWU<>&Bs5%rpLDciM`db}UbDJ3n zHdKye8v!-8tSH90v1KpTb{#^CqJU!ooTJXo|G8w>XZG&e{FAQoM8)2BV$&&Atpu9t z%jrEUy0(!l{Rf;^Y%!{Gb)I`SpfJNJWzIwM`j6IG?suxZrJxs7bvx6D#3T1On~dUq zTXV*vwrj3so?j|w7P->t?Uo?!X`cacdn|Z32)p|3N-fo2HAH^endkb z`zdK}Cw2OorMp8Y$&^V6_f4Zu$@?~LC1Wvc#hb^IF95M0j`RH8s}>|A$nllHE}urF zzi*iYf+>svF`m2ih4xcaLJmPtw`gZLx%TfAxPA)Mwc>%Wja6BzMq1A!hNfpn*+MB;yrdMv!z^%asF7E4(~#U3|CR*t zlNBI9WV#wd=lPXK^|>nD z9CLjIDC$|_Hs6b$?KrdY32l7GPS4?6Z4OP|vnP{2z(>y5mak(I1H9XxmZ{z~UdZ(q zr!+G$z?!kR0NN~xKxVCKaCy@6mh3K)N!-CtlUQJ9_yJHgUOlC6;&rp?V%Vz2^fr(h z1=Kh<5z-rFtY|iQ(rlSM!I=1a+egpe^bO1ldRr~RMKn@mW55@Du=EK}hwG!6=MY!7 zsL7vp-yGf-J}?;UW;O}u!7yIXM;tG0Fzx&cdNh+Ar(KLGh%e?Z)^Xajbn!#VM~GAT z^vWxqM>bYLXuTBEkKH(NPa0Ribyo$j2;;e)DP;ZE=IQxJQbaGw|3)_kGV6j%&J6sy zX8>zj=EB3H%HP-jXDCVR3Kch>lM(lzX_ksMyX`XO<}z|naO#WpZ!nKf{$U<`PSFQt zk5X>=g42;)_uol3Ou^6^E~fvUfXP_<=_$8966#Nn1*|gW{l$uO`>?-g{g&Fks=B=f z^ZT$77gq%N=vBol^d@egn1m;YsXbQP-wWfExl35gRHX&K`-^p89i>Y_W`g*!z@9sK z6(cPlAwF};Irg5Gir8%CQNIJ25i>7xzY^5#sH@ehI$@tBgb^a z5s^SscTu@g>r-8?q;iGFF}uK-_rPU*ZxdbfNI3)^7QqX&a}%H@Rsj4>X4_bC5Jo|u z%I^2S#gZ31y!daiFe2;c3u`(UR)4n0Exj;wADXgTQ$udz;kI?d;jQgHFpDoxdh|6s>MdqQS^MJi_fzVA z$XE(FcpsvkcU{OSU2UcpJj`s$hv_ZSLzR9%Nr^sxuDsCMze=L=qt;%o2O{#HHa>Zs zK%^o*Cv`uAQMI4pXL!P(BFn2YJZp;j(kBw+|1tOw&`MiYyzv8YN0~uK2b~L`0;Cev z82|#Q3I7l@ZL3+6-K6}tH+U=q54FY1Dz*0g%C2=7wGP>!`!t_>2%X=Yl4B77Rwgb? zSpiQ@J}@HwyrQHnK+HRfnrN{HF!?>ZG|_wv?xojz{EC_$(YTV=6Vf#_?-t#|@1jCa z+21JWa*PJQs;a7Ly@dJs^lf26aj5KM&$>LS9^lY#A8~ML)py+boSr>#>k{&(=|DXWQ}%B-#;bu-BLh6MfU)fR-na za}J#TPWhkgupS@n)OUJ<+CBUFGeGRjX~f5MiTbv;wYGv?Tqx{Uy*{G28`6*7Ej-T} zb;hkPErgrsLVqBEt|6z6LS;avnuSJyh&C2u_p5SI@Jl|%&lwtD<=i9X;PXEo%{y>? z_SR$hm4i0foO6@gDn;A1w_WqUw0* zn6E z=HFpbKx|WP4JDOm#j04wH^nWuvWhn1z6DWQy2%3{+iX_~M7$C>gV525ydnt%#e|dN zEr$0{EHjq3#|miM{tG?v=a@XgV;PhRU(8KeZT{(X>29&}Hnhq70CZtNz>z<&?)?q8 zg4t;r+w#VnjR?+kv8&&SnkaGGgaP|uy0wA)DVpj@cMpe~g&Y+5uRbu+e>0#H~{0k5JzbK+JvKaZpnxG>e-c_I&^L@<7BECW7P zr)k4+(YS=Y|MOG}%QC%V*l7XzE*hIQU~Oh9{@w%El{>!s9($db)01Gzij4J`xYh>gc<_?DQmLN;Un!cvlv= zquj0GqociY$0#USgm~u|VY%`D5%ulyO#ff}2x(GeLoT`HHq0fGq>zzo z+ia-KHTRh7P*RcWTyo7Vx4F$*$bB19F6ELUmRwf3CYMzzDjz<-cc1U?_xNp($Nt&l zvAxgbbzbN7JkQrT_6EKsWJs7?UO@g$*|>Qrm;oXPC4?2kqae14R3~gdv^?2j3>_CO zdRP}PxQ)xo=&wP_cFw{@+z$XL$$Hk$jAQ?1y^|oSS(vf){1dU_8$WjQ1om_)932i@ zC7(=wN~sEGHa>QF@M=x$qoJr13mTNK+_};Ir~;UFCFXSy9zrr~Ub3|H^=#e^?G4_M zoxTmrZLM8}LW$(8csd6(;MzAaUcm*~?%-!F(%=yG55LTYPn~MNbJ_Im=~#Szo!5c! z>9bA;+s^~)yC2k94*fw3r4-`Iyo-$ zR4`sZ0DoC=ZK=eU%YKsUq2;s>Uq_Y%^0GYX<=p6-KNUK9*OYed$X&G)mO&&MJWhUu zY;`2aG2_cPy9)2ws@f?Y9Xnh0Y`6w^Bm+Pw**SqpPhLJYIxf+~!^wuEmHx`*i-I8y zm&OI~9JI1s-%iS1shmGU#^uOaOT{N-9NyT?|77)Qe6KB8DGP)g@S3sZa6db|^TkQU zwuVy;S5$^9o-Et?IY7Ut5%mm$`Qo2{Fb(g2vMyYz{kZwe&H7HGvgg@H>OAJht+OA{ z%f{NPfRt3bvS+!2o?abTWm9;=bwNl<))Lh~8Pgj;NcC7W=5^?95s9@)CF0K03MENnkUmV9s>nx6l9l*sb6xRs|Pr^_P5NNOs!)ZTs#X+*|Cv!3(T%wlnbS%3jkx?fq7`tGY>rCIl z-e+h-P#Zn-xI3e7q&*aZ-|H!qeqmwO0M=!|7@w4X_DCnt3rW68wBXBnYo9_K?Xze+ zCQglo$aZBGYLIL%-wzlVJYfYr?R%o*V%s>}qObI%q?{pismbKvAFSz-qM=+4Y(*lF zb{W?U-Rl;9$a$Br$h1KLxyC%g95-vvidri}R5EDMla`rImigP5t<*dPeU(gUeB~)< znEA-r#o*D_uOfp8j4VJ@a0r#5SsIY@kCxD2{%|S`mo_#{LPb-gK4pjxpJ~SU~ zO^-L?8F%v`&uT@O&Y!S+z2J^m!@6VR?3Ezui7K^8$_Get6=&8ei6!jQ_c^b*-ZhB1 zJjlh&d?6+6o>PI;nIDHznz3wsC706Z;W zF3rp)*?sm3G^h72N)hgcy9q}kg`rZ8_2u-e!XnB|9)82S6bqH2mmQ((^@( zeZP41LIeilE?!?D1ugiXVf7&sNNmpLBdG3Ut)XA4e$bYeD%%?X!NO%$fXcrdl}gf{ zFS?0VQn!=wpC9;%c=JCQTx>o3=K8OiZTy8)!@NBM-0#x3i0%5v+kVjctChy@XAPzV z#)6W}W`=DKFc!#My6-d2^t`#DYi#UD@7-`>=W*^jCFj|=^;3K&WhUC0vPVs&Vgs}& zIT~lYd4#eYZa$J>9_*^mpe(nrLhs`osm3O%IWVX@mBK&(ewIxrZI`5L)oLGj*(WK> zyqmcM^;PKPOQ;y^;dR>afUr6HFL%@;atI0>J3N=xDh&|FXfVVQEp}lSp$$0bY&^K$ z@AnN`V);9dzaa~9O~GxWdXWeJ$7$43A&Avm&BKIrI7H44H%+e8j4zY4t$7R-!|`Vd z%3bn)b?lA?U%9HXf9x!*!`o-n}uZfnu)KHg?L&2?>wvYmAj*?rKgT$#<4x0lr z!HG!SQ@Xr|;Rr3p>mco!&z}?>pka7Un-`%-MOdqmnWB1t7G9S}0l9^TRx@cbYoAilOL!*T?jHa+0qd?^kfeJ$_ko zS5c39E-Blj|vyH;y7G+z*3L_SVmZ~i>TdCl^r|gsNMb4T;5G- z?{v!MCgEJ%AjA%@2_dGh*uX>Zw&ZD7)G?FMX83_#5Z03`>$be3J5ItO z`JC{h0}{jTt7(%qqd5G>w^?0Ejr?qy#Qr6&x38HYc6z<4D{R{B^%&p`vJV>(Fm&xF z;DiC_Ds2JNddXN}jr|rn09~e=~?H|HsN7N0G zsOSG+9x2@#?i~;fYQ*XD@bWm8;>2vi@Tc&WoJK`ua@EAR9ZaV$cq&(W&Oup(i3&p1 z;_RpOD<|I;rac|GUcJ!ynWmmm$K$k!%}W@swL2aB1AXqo<}J-@C$t~8$Cy%cmhsil zTzL`;mGb%sN5&>J?Kard`)k3^h) z^Ezpm@K#?E(2SFiy0GJp zXe-IYX6bQbv(UI{NGJd2!N!E!S2Pm9ON~q;s#JLd>IUrTl`1F);rozjE=RlZSvC6> zxBz9%e#&!V-2WNUOdA8yVvrSo1H_ZnRqfGG-oYPsc5sn^*p$@J%bu5_a@%ywK6UX2l!Bl?1 z!Wa&!wGX!BNO{1Od*!Ipm(!C9V$ar|lXLCccp{ZFdk49J2zJa+-a&SIiD=|ZjWyz= zbM_k4lluOU&(uKiWLY!6CMigAi~7GDmYgYZfH!c@-oWVrRjbZ7jGvN=MRBb@x5N^@ z#O>f^&Bn)003z3f*4m2^WIutK^C>^>BhNKMLdQe=GP-9ImfI)vtg0lx2Vn|xa$YUi zz=QaG2K*lr{(1RkzYKMol2-ALxpJXO(9!NM3^xospIB=IR{Ei`L>Ju4>!=1qcT z(CKPZNc?*$mDs}Q$YPTl&X24RcraY^fI-s@`?JLBr1P~Lt4Q9`2bPkVVOY7Q^g9|j zp`th?{YM-XK7UFc=Cuu+>&>tUIr>?SGLsy?>i2kZ>n8?Nbm7g>1kKOjvEcsabD6S6 zu(XW>$T*(9LQ4hScb(qjZbIklV72}0Wz4oy+bB(P8Wf;Z3|Xn|Xro(iLDjCXvCbCg zU`~_KCcN#aK0-RAoq~<6HYZ9vn7z^8Zql$|jN$hk#}K_&yz+#^I5AID;|>lZeg(|K zP~BsAQv_fmz=*hddvbDe)2@~F9#;$w{CtLm#|Q0AXe@`80fAl)zF}<9zg8Y@o``m- zy_UG#nKEwa=GG&j`jX^Wl+|GX0tqQB?iJBTimi@L?Q0H_KLh% zg|3>hds}aG4%bp$p{JYsivW@MgwYsq&X04-_)V39+J>Z`W}k_F)N4$CKtwKbpo7T; zh4J}`K+2GkTI@=g-84oR$Diz=+95C!P`wy13PG!SStUE@ib7^WDzSGZ@>aaa1h7+X z>kQZ+48_lH*M^d;iz=Tw$*vUsJ!DZPD}!~N`C+SVo4k~^tc|8EZ4Y_XsMdyV>8Z9d z$j1D!e~oz6D--vGoabP8J{x;*E%%*`h~jyXJD-j6zoYdQJbKs^L@3WCOi8+307WD` zvlR7M?_Z9AuQqojqQJpaLghgJ%y-l$rW@(bO#g7GpVkIe+;glUsU;sBoM#18Ht~CR zAX|u1yM;CGFF@wQeEqf6>J6oGj26n9B;3lbqfBStq<&wkc=e4(pQ@~}_5=etUZs1? z6)FKPXU?Z*d0OpXjk{FdR9VU@8J^#PhoX+L41(s`|9BQHQr>x!1~ia{{T8unmApR( zmwgR|TtD^SR1-jb3T|pyNPUfr@94pgvK+8`2q+F_J9Fr3SU9??nn28=Y!AN7R++-A zc#`JWqTCVg-+8>NeOm!~FuX0KGdVb33H||Touy~@@>1^bno4Y4RtIL?utLv?>Bde@ z=$)?msw|Q($T(5Jp{rvy*7q-mGQClqfSy#AkO?z>dyiKo{($p!{%-Ps_p}kyctQ$` zvwxPJ@;#2!{V2eqq~K(=y`+|7edYUG;_8FuqSR)sGQ4kZd05OGs zxU4!^Ym7x$y2?xFt5?IH`Oh@i;;k8$nn~z$7Ym<8GUQgD+1EXk$67_+lwA*Vpghbx z60S8VBR*xrWp!1*n!ePldLR;9NPmi4Jk%w%*2-quOUf^nYPMIOedKPt27RS)ff3jH z-0SYKWZ;?kb74D8$N8%7GAc0MLnO!ai9fZqgEHI=_e^u9kmGKCX~fmL21-Xsr;pVM zV3Qc47G})-VUfd$wdmGhxR-))1tcXk#PT{NS!!}6pBr9OW~5ONl-sw1^;@D%9mlB_ z3~ojU4F9?{j0o97Vjy0>c}%tPYfpdst4Gb?q9n9R|NFg=3zbC5BL{;jm@mvIUKCEL zm%EJ(JX`GxZX8zWdE;7E@|25Mn>vS-ZG!FjLDwZlx#~-a6rAGYD2=@4M}`?k^z?4o zW$TQNoSWXZYYOz7kDk(1QZc-=H??x53!HOjBWeS<0=e3vBP0hwOe7|x#`DZJAr}EO ze9VHSb;db=PboOM3q7`Kttza%xDPnL#kJ9Hg#&x$axq#-Pz%+JK4(aqf zJ@!J_%!EHxHW+^R9l!8v(qk>yDb5GRwu+zmsrhcBdXoYl@T`V;y0!HA>qR+t`BzjE z_a_+rJ_^7z$AvoY1arN1OS37HT0`U_}zh<04&Ff%%6E9DdZ`-!d zxPQc?2qe-M(d8U)BF7tKM3cI3t*ec{z(b@%m>HL9!kWCoxJ7^k?y>@rxCM?^Q~X?L z71uY81sWWfSa?b;DX@rh+gT8+XVKT3ok&uGDH${amDqO!OxA(~KmxAT=f#ER5``8d-^%(|SL+KJj#Wb* z#^$F&{g|uhW?iVGlM=V%pGO)sIQ8luW%i0M z%Ba;L#&1sCUhmaARf$lLs2}ST_S8M2;=-pAOQks+LOpfhnzt`H*DQs){~5=h1dm4t zGw8n2f$&hwsC?uH=o0HxK%2$Ko3PV7Pu*6_(;@cd7XNaPBg1JCyNe+e$>|n`7e0B(DnH?7uJ0B+~_fm~G&R!MTJ<&JqEe}zd);)33 z@q!@ds>AYa+4TKo<=oZ}%|%dGcj~=?qthnkt^-F&aeVx>Bp8_3mBV0(_2bHxwtSDj zkzry19h81n9?wCGVUbuT`;qpWYT)8fy*Q10uJ@AXbbP`P(zp)xDE6L*kbIubVdI~} z7Ehceb3Q$=L$^lywKFZG1nGZVWU8wx^$s)jj1qo=y0Qbp??q}ze2?rdB&=^{phE_H zTfAULJr%)|4X3ks_CZoF)2uPCd3hQatkhrVDGnzp*G6mOaQ18k{3?i#QxkrHUPPM? znQZ|BxEgu7HAtX>`CUFE7;-qwGTFJ#o3XY$SzB|88hc$e?xKp^x)xC4l+mNR zKO&G4^yg9F$vPXf${=AAx>VRJw@`WNVJfJ+SjrLr(DIJe|6}*VCFj*zpr-XikajV8 z%o#^qtEEY7r9G3l$9iM(X{2iVPx*k)a=(uf-r=CuSoL~BiLuYbjXT`X2G80M zKnT}Um~G)Kk(UtFz`1`3Nx9u2Fi)$IttQGwK<4$|9sO*}0qE3D$IoW~UyDJ~+u3H! zKSsD~IU6x}dq_BH8-&@Y?Dha`2{bjD2`Rel$puxUQ^i!;fV^N8kye2``NCQ zxng@8V4QxJn@`_w3p((SBlekVpRn8~sN_24kZIRKqAvL*M+!Y>jC<1g-cXAt%R&+tw>P};mI zuIqk^^on`p3Ro>VKb+V+AK~dMZ(XuJtCo{0n!N2SyFq(j#BXjC-)~uzm2Yg**@%A$Hbv&ofAe2?`JS~1qrFeNkDs~3DPy^+} zXqq65xIb}Fmq!n40s&QzW;M~j!W(~irS%~fkCIgFVN*7vqB>Vm{iegzt1|lm==!23 zgSXv&pxVVG&wCEupM=UMqi+p2;?3D5JG5JhUt3kC#-)>G4~5w@a|dA6>|Ni08~KSf zIqsiWtUhvVoi7=K z;kH5)3FI;Bzynkp>x)K3wj@4fvUJeUm_&F47pF+NLdiBXe zaJ43L*(9Zbsu-BYcAl<{#)XVy*nWRYrLN(2E#98 zSquc{;>>albbCL>JpWqdkFB2?#BP|(Jl;h}o*(9&R0pYO1hm0LxHzAEnH+Y+!@*&D24(^kJm)W`RtA!s_Fu3VxtTAe-sd)*kv`xPN}5jOU-me9VXn zb*4*i+~irwazK{D3eB9N*gL|12W=a%aN?7Vza|f!4FA7M&W@u56~Z~nVW)VW-|a^W zBo}%>hkerJderZ#*ED#B5r#8R%4R0232>v+J0KtjDtcwVL$YKl?OzUto~}gv+lHmb zS9WuGqq?o+^9)`#YH1ce!VU{7Gq3jQ47-int|1Q*!Ujh*vVd^2F-bVQ3o ze$w`raKho1+AH;*!{0(13O|$3gowJdF`oHpo?CgZL?Oai^ zXe{+0oQ3Rl;Lp>=;qcW|Q9c5k3|uDo*@}*%F>5)t3jIL!So8yCwt>b`uy5lIrWI~q z(VU&{z#Tmmw|v^5ZOpTrGx!Cn`mMBQ$FFc$+b`#lm`zMaRP@^yi?2ydzs4M-in<># zi)_i^mi{3HLECnr(QijyG~rKIut?)>^LKVJ|8g9w(J(2Km`M8;2)M)D6w$5-pVUkSrYh>%o;wRYCDMfYU$e|lX9{Wopp zcQpGv{{1$EoQTxF@-Ii*r6YOt?IKk5#c0^ITRj*pcx@f)>gTM76S*)MfYuP6helO@ z&e;M_@;Ct~8vxEwX@ux8g;LUd^aNu2{n@Sb{n}2sl)~(XT_&^_o>tmjiW&!-i?e%w z_by^u{W1ed|8jtpAHxR93Y}$C{k-w8N^6Dakmb1LIAD8_Wf7Enx&dajM#}@k%BzuH zNcxGj%R!j-6Y~}u@U-*meX23ft{(VPg#9|fA$lyI$M_{A+&D4_gV&Ia%x}Z{AHlKd z>NPx%`%Y8^&*N>u2I1~MtbYX%m2d%9xL2EI!+xNz-}fAB5UXfjVZjs!kYC4SF3sn^ zba@Hz_&Ce+DRbTthm?#Yv{w| zI?uYe`pUN*l#p#OCq=0Ku30Mp<_TX6VscmC6ARaaxBBqC%4IGa%ya339t`KenYBu2 z?=gbP7Upx*wZ3?>@-VyuW^o8-zw((b?y*!A^k&8BRnZHN^Tvflswf8mlE}z@FG^m1 zZh(-`?h>heSL*r92SzZ=OM;d#M;PVUka7l4Wn)vnQRBs&2Lvm(p!MK4D!UD$2+?qC zPa$wOEC*IqF4P*Ep=>6W3;nHjqb{0j9wTTLoc7%>$7F?;Im2VZ@11;6|H65x$@)7l~nw9t^cDko>$4Pj-P`l2=LW;Iyu?&}iggn!&!d~+;ns40f zz#R3GUXDm_O57g=*$!uCF7t*RvAZPhq(FG6d=xvNo&KL57&nMQ(?L8>lf^PW9pD~M zOb*uBgzB8SAyZ7ZE6vDpkWdDL@re=#a^Y);5#aEOfX#x=is7LWmbuE5(aY#%Ral!oe)_Hchw z7&J!ZvsdY9yMRiaI#x?y7gBE5meNBodVhr_jcPE20R5v`;_oLzuC7Nl>U}3nf@`eN zfZTf{{%b8vY=mm+tb-A)vl?Pgwhv#I2;w9mZozxUOav=g@AeDCjx)BAN@4mG-ZL!yT zH`386HaP@%CfM{KIV{Vy+11C~^n>DNgxRitku7IrUvqQsuZPj1^ox?wc?WhY5?2tt3p?sxnn8l+LNks_S9u&sEc+*9w*^Yb6oaMyyh})3rHWizd;*b3@IvRUBEuQ zgL}^Pop#&>IQu@3;>(q&vUL$Z@GaGd)8egf8<&p^-Ek;8tZj8tEBC4<+33T;N&I4enr2yDET|sSrEt4PhNaySC9@(F0M6 z59O239pD@{g}(i%l2rX$?>a+ocKv~k!WGi!+zghc**I_N4u5d1Mka)uAdL+#+{%zs=J0YS5Rt#XkSj@)8O#9+d zoQ`#F3Hephdgkh?#zQP3)CKl^o1n2lbD2JPx)4#v#04wgI%n0(-{(qD-)K7Hc?J|j z0~Q${aN*Ya-uQ>OXRGxM^N{d>jV?`^=qL*}Y+vfR5S=H6krg>xZlobF62F!{@|saM zmGpA~Y8MUN=>7VdbUGLF*C`eQNlHgn3#)Uvr@vB&RNQg=F0<3f!Syh;40gYpO5Jj;E4*qvOfJY<@3<|xk5t$0~a{*K>q-r7IZWpSmo`|S~HkABkn ze;-^+?5|lBV+(rp!j=)czNijmvasbQ2w4}iL`&BV;4deLp&^$12O>X zEyD{&ckPC(K2oWA!JI(=+(}n?F6=!x|1Rv-Jstbb7X+Q2`0l)fS0jCXAF-+{1mv}& zWCh)8*W5)DmV1A`Zkx+f0Hc3{v;0(&L(&uE>39QO1KsaD{j~j&em|n9&Zepjv4yD% zHS8*CW1Za%zz#z2S5L8A7qFx*7Vb~Oed~mkOTB+SXf)T5^xzE&IFJ=xpsm&A8^q)Bs;AU*{oqMQf66N;%I$`bfX~Zv)`Sddx`7Uar*4Jj}NqG?ZM?<`S#n^VAHd z&?g!Uw-|no|>vWZ8?}h|b_lMdtQ3%T*LN03j!jAkI z#0AFUE_GqI7U7rJe6xiBF}5OY=KiT@u%!zOJT-&FAT4UUYQi||;I?2LFoQu~Er@(G z4+OlPoU@Np^QYhEfq9D`VxwsI)lVPf51+mRL^jnzVV-Fw4fgX6-l6J>}i7tv9(H}H@nhv z=^Z^L^+NPPkL@9oLM80t8YSL38q2p!L1_Zpm-mK-3rZfbVJKyGbTf3*;R#rW9fmka z+&w_HbUl+}qI!{korJ?7bZm2lrp-N%A4gm-^jx$m+RY{gNdzk1d*JO$azHv(46jkm zemQ-~1g3CEq*aZAa^B1mAagZdxkGl;*tf2JO*g<17ItT$16^UQa zYhIMfIWO}s2h`Dxzotts&8`bHla5l^fV}Ob8V<7^6jJAB% zf8vr0;KCDN$}yw`a7I@J&NQ!yBNuo z`eieYhv#BwZO+@TmpJZ2kXH;8yBAIi?}`YZBqywrS`U;00atM#F~%OQL|ae)h<+}gD9 znZn=pld7S@72;I4Fu^d(#a(#YEuew{M&~6(sj49qJ)^1@5#k6{?AP;ZL4U0~7znq>Tq_3r++hS|c~HjD7H5miJ~4Yv zWIObfqh?x(4*-{^Zx7nFy)wtQ(HlyJ(q1z!J=4P%-{|f~D4YtOH4l(#4UhE*Y1wf} z&^n)IV1571Z$1^kS-S&2?ALmM8UC7O?R+hB3XPc$!{b`tDJ>Jn`1`#{3r(j*(oQ)~ zNzHdNDtZp}v|asl+$m^W??U(c9W5#14g=&aBVhQGb9RY8xv`D<5|sa(HOzMW)PI6K z>Bw1s)TYWh{b_&lujQm}&?#i^6d%QE8#CqV3Q7cMFB4v2TOSVORiQs)p#U07pCQ^7 z97$Twqs(KUwAjIGzwMcU)9ce#c09CI-WUqEg1@bLXwOnzOZ2KlaaHdppDI;G1Gxz& zdK!UOgdgcol|%`J_>bOrZ-=Xj%t8L3y%)}@$x^T}5xsCSTZxIoDvn0M<-7Dcz%Y8p zukDpKW1d3%^nOL@dc>29JUZ*-sO|{$@vGNKCAADCpMs^m9kbo&z!1{g(6#OfV(VV* z=fN7;%&A-nRi~yiCbbD;x^s);+OGt-%!N_C76$+^N}5mEf$F=Q2Bu$F5fDn%`+0C% z_%XXyYzayxMLAoZWygpx`NiYAn>K1DYYrPrznD{jxU>=Pt=0}YX;|8X$&yt>ez|W;x;6j??ZZw9?MRw=CdU!?BHlwiL zP3sI4jg0b$UcTeEtuk&b0L3l}QiR40cD#C*?BYC%GXL7~ue60In)1^uYIP|nE(*hr ztAO$?Rmn0hE|}Jyp-d_F$M-uNH}|g)7NahlA|Ou*r8ZCBUMG`YkMq6XT@Rzs=SH(U zL)&d`!Y%nqO$XDxwqnDcTxYPX(kAJ@O<|U60t}(*79Kbh6*QTu``t_zUjDwX8!^ zX$Y?Z|DKRS<>#MP3jiVd&ZI;y#Y^~*O3ug2I>?t@WyNO#;7E+9{TebTUa`$>dfOKJ z&%&f${9PnyU#KxBTIG#AW>Rt+n*Rs3W?q@&6QldNQLkuO;fvatE z)Ur=TRITx1CM`c61ti4$^>3=r!-Ay?;9R-r@wdBoSiu@Xy-x(hhkz#g(JN2e6UdJX zzlo)@3d3h%fHI}K`TQ0w+kLg-NL^}!9s+#t5QywjojW15ICu@?(+y-4Tm|#VmwwhG zJY_dKWXSgXh~$K|D}&Ocv_>QX!GudN<+5&jd7gM9b7UX)7PVq6{^f-FgAR!g6i>DZ zJEi`ob;>mbS?=%~weg8%_FjVZ%iMQDa_7v}SZCC;?kZ~A7Z~~bfPm;(l|QvAADl03 zDnA+9!DtC~eVJamLTP`c-7R1j2IfQqUmoVGq+7=5Uq$MQaU#JwM1u&Ri%wa-X!PIy zqGM~lCC%zS`yp_*Ap!`)!Gv{GStzILl$)f1D39A6MTP0!6}!!-+PN^AFX+ck|Cv%x z*DchffULO`S5{pAa#9ICY7{?iZz;5cTB#X5sQaD_8OMSD4+7LO_BZDOZHq#n_#k94iJkP zqw>4I_d@t;RIxhdIu4KR3t8TPxRT-x&fP|diu&yjj2-9KGY;L`g-R8{f2CPjsmSWM zDqsJT(V-tzP62XX*o-tAAoqqk695?*9GYIp0%}zS8Gd1?rAl|Y(p@T40F`J}RBiNV zp_1p2Og&!4_md*Udu= zBDikE%fwgJTm+?&V#@KC;PH|H=wGSkKF3224NnNgB4(yBH^|ykHaa+h)!)Ske}g-4%=~i#Fqb_z4ZS*J8L7Qh-u8XpB<$BIW)$$tD@R` ze|_|2_q4ib_qV}K#P;Ck|MbuBs(M->mkRxB0i1%Ea$ATU#1n@zbWmzHdenhnG%*}7 zYajR!&{1tE(ZgO2NG+IS)Qy%-eTlydRW&}SfhUOyhmnt|7JiD**%D+5K$)l+I8hV% zY&kMMa@q8}RkF!Bw!6P&stFYxI>QTS<-QTcbx$PA$JNvsLj$W_HV^$_d39&!mTdFx zUYHByV@H2!-+R8}Cwy!AS{R5ek{iHhhy0oA~fFQ$gjuSM*8p4Ug%mr%+TbI zLx^k>IL!~Z7I7Z(I4j$BC>J>%gbcxrN|TVIv&fRhjJ*AvC z@-Oin`ZNWU?qYEeg=c3^Ud-*~u^D{Y@AjXLGwq{3xJ8##e@*}NemB4}l^tF#w3eK; zKU=gac$sM+-wO5}U`3J3OyrViilwF@po=eV#lsa>6O#XEkoKtW|8p94Q5BfZ)N!LQ zD7(+mh2+@EJ%>iCxCi|?JSZ#*XLr%6yP3x`wAFNON$Cb==+$T0DkiP9`g6u=5r=AY zXeSQ;yF9AZPBjTWfWz@Jv|@kfy}IiY5(X*xxtEy? z_(Ms)Wu4u|7>$y9v{S!F*;TjMG{4c3aEg5I(vAz9)OsIjSPa!Xo~!r<$!~vUUPlvu zO>h;`{;s(={e}a8vvIl&F53$HO<8p|;XO#Z$rt<$P#K6$TI2!h~adicq2> zLV@F>Wh?So6G!cX=Q@tYg69^Jt|y4|4jflpjgvU8v=PG4Odr;cr6irQ`x_vfKQlby z7H`;aUJrh=t#$?nWfwRh^3!_tdC3ZQh%u^AxaC-{C-A zp65vO?LKn9Jv$2MwZhsFy}4jn#^Uk6_))PNula(Fz~^ zc0(WGPf~oEEECKlP91k!YR5r)1!Cp+1^{K_D{J7k={#6xxPHOs$>8-U^jyl^r{`KO zdL##B_mJX>KWIzVhlR2eE*nh;V^-Vi#VPy_CnZf2%zd*aMV{-I!7h`I|4lr0Z0;@G zRMLO0V0t{vvcL|}{UvXgi*!JieVG>UE=k_Q+mWRIfO2H)q(#i z%YD;a!DOL(Mn0Re-7|eY=>5Zcm*qxwXjPk{4(h~ zoNmE(mb=^PEbkzUyISnER-;=UiHx*$mgv+FVC5a?gu^Ztbmn>7CLh)uU>-dE*8NEx zh^QudqNm+5eqwhU1ser-oK-s;kB>YL`DU{qm^BX;Ra=_B=65MJ@~NE(`ZHj!guUix z79aVV-L5{!w4K|_OON7NEO~ZuLDHqoWK8rAwK-#$-$<1_cpYkHAkDZNeg8LnU-Dm$ z5Vh0(((mv73Rl{lihKRt*7vUOQhYYeGU3=e5?oztXUHl^D+9fhFx`ms3%*oB4v&kH zdG}o4IK-o|X4;~9Ub8BJgXE$SW>s!_osgayopX4UJ$@W;l5v1!Tg#3I88m;4@NgFp z?x<1f$n;8D!+}e^We*sq0Lh&RzM*z@$6vl$tMLQsFZi9UQ^$%w+7R=Qvtr~XH#)02 zq%#-eFZ63jMJ`8HMK++H|NO2|;6K2(Q20CVJRnS%P;jid9x5~r^X_ii%j*j3rn2!j z-tKFPCJo?qRQiU*RT5x*-qVrxPTB2ZI)fVzW*(Nv5KP2@l#1bRaj2RFTTE=0mVYw?_tCa(%T^g3Tv^invjMI<4J2SS;AplofP}QJhhVR8?N<(qMeOOzQJ7 zoZ7>I3|Fxf^BQ5V<--@E=Z7|z{C}7chCdSc<1tq8HZ26zxQ1gi%XuvLQcc{T9GtdB znim$CI`E#uWBxA|HLn^!eBb5lfT4TM z^=4YEL`(MU;Ph&G&B=obI(%3V$Fkd?MkyO@V-52TZO|@z_D0DoTyCB@*qoH=pgS6B z8|RbojO)5YfjX|z&c(CJ;a`qv?47K-D4>NQVV!Ao%a|2e{=au{{EKa8rRm60593ya z;M{%(;SBjKCE;9DD-3;?ka1tNx>pCUabi6&NJvVf^{Q-b3{G< z(yUp#8$-Br>9$WY86YA{&I_fnZRyfrKzU^=g9tPdc?t7Lw5{V0oW*PVWtwI9?lN_+lekn?Es!jOIyO_mv~?IbE5EIYA6n-rjA zCvPLmC&n*|C-(a-dWZc^eK1orI`L|WMDs4Ws6I3$|HEbW28ITht;CF*_<tJmTx z>2lpd1iT^C2qoZ5KIKe?WMfUd;1N+v-fV`YWdJ%EB9!%!JmdLB4XDqWf9xzp{<^UX z60s~3|CxHdZ{;n%5QZu^@wbfiWcW-W6)Gdj^>waGY1>(1S3_VgkG~Txt$|4&rx9OW z_WzedWwJioOEl^IdVkW?G!sq` zDb7pu0dLaqtUtZEFSY*5yK%mxCn01XHu4M8liX0_Acw#2e!6*Ru$*79R^61QhZifzc?PA zoABTG=5``w<@``pT2O0%;Bj~Bj9L4*0$6{gWyDX+cEz3lBn`uRxpD>n`kIKK&PP@0 z>MbyBUx5jwUrbY8Ir{1LiE{tj${jn(DVXT1*!tI9nTqccLkflN-CGu``N!d$=5aNz z>Bm2n4Yy-9S0~kOD+wn3xX4o}U5`yhtPP}(HU#N1`At3=FVW;-ia95vbDqeFjU?3h zmerg|j9mr^SB#LeUu>7wGuL+m^)97Q*JDy;K$>a#pySL}J|~NZbmavF-hVkdDIP*N zGlbcy312%IQ3@CmLAHLlncw4;O!!>@)@%)B7Xokq8CX4!QGMfH@fWh;r4xV8bT@Q zqqmBu_}Da%W$-uJ{f#3g)P#+tv);cNNEP@IFb#=%XNWEE-8YW3Ek4~p6?bPe^1?rp zkL34Or>~wmv^NsBLy#st+i@2E+6!h2B8{9CSZDu@m-@!;p?Gh~y^C$m@-G-NSI_d_ zr;({3R_M~aK=6!Acb4yL6k<&!vDedBAafhu0cOUKa^tr}rdwuj`&~nCkih3T0!B&z zVOdWw#<^=4Hwp3kK8;^!VI(M)k_0V_fkk*tbNL3%VjKCa^6_JqPTL0V3N$Nk>&y)po9a<`iLen=}7F1(u-#vOSqNrJ(peFOZP%zT}Y^Y6q zxzfYQ;48G-OV*G3^5n|wQf{N#L9%|egz;K?lVho+dW!v3-CIg6-s89QbkdD--Thx) zjLd8k$^5)pl-$hMaUdc25!e^l?{2f}eI=O<5o4Cgw+lxMbN=NJzBX;}efp@a-^;eK zyDMF0je;7Y8|`-lKLR6OKg!Lxn-=Qf$JnWo@7KR`9;xJ-j|e*=tb4lXp+h&5Rlttk z(vNa>uwpdsI&G5BTFMnLP#xP3j~TAV7)Z1k03vb=h-$`BSJ+u=C9l*#k@^(mLXhEYAisH#5Ei) zA5KI-fFs z^{&%kZu@xLALS0ug(Ni$N!~9XBL63&DtC>PZ7JR~M$14PP%KvwD(ADRiJZ^*(m+Ch+A_LyOwiP959845=JeyXfn zjaa#j6eN=xI;jI zw;y)wz{^k5?`jtyTKr3@_nNaG8cxp1V+A5|umpu57C3dWk7_CjHy*C|pY+}#aBUY(eHEPsWySAb> zRaK>Qe150jU-C!($#tD`z305|^E~%+-}mF*{R=#7Q0F=ox8h-*UTWOx7eMZ`QW=2q zwlw%hqQ4SIAx}1`)K3;rX@|9WG~j2|8I*vK6U8vl_|JwWLJWwmI)Dj5fZJ) z(3-?w1OB^o` z%$3&iqY&J{LdzRnO=tw5Ga54~2{C?_UrT!(O{AZpb`@%+@7CJnxg^fkT`YeL{cgN! z;d$C-3%0KSa6}k@QUK{SkYzJhZB;vO9tRF~4LqbCP%`CPYf+AP4!W`yJMGpI=x$`V zwx)esYyNbh9zQD)RbQ|IpZE_iAuAm?yai>RCv1n_ysceT`?c_8?JxX!{F8u(@5Kzi zIkddonf}?dBdj1oztD7Bz2U70lxD#mfZ6Czr+X)nQ^sTNZ37@2_0tT*&%lBEHF7^w zC45YuT=tR)qnQ_~S9<5zCvn5TG68IJ6BEnDe`gm5Q*D`*yILoSK$Wkk7RS$|cQNGz z(7cW2$8K-2Ri+pk+p2Z>GG#%hKH=@rH{S@*>BgpaceOdA*P&b-IaI`=!#}Z%ha=U{t>8eY<4Og9sdfbMy>dWP&d(CTXR@C`hchWrU1yVLq0xXB!H&|&(Qx46u+*7#AqmW zO-*f>zZSo=Ed#;hZ}{RwkGA{o(M=#YlAeAo9Ub};r14>!+%cJL+=F<<;C)d}bT^M6 zZ=i9uHk+~rwX4M3aYIf=SflK5+iCp<;G<$GDVRL9Ae`XL^EA z68&KAR$IO5Ov77dl~NjDyb*)@_^lO#!ipMmSP3Uo8OI138n3piRRiVfzjLm(D8NYD znY=PQWVlr|t>Jn@L+dLRR<*v&+>@r9)9$@Qc~f?!7tQtIb6J(Dq!TVOpx;d5@)JwS zbn;c5-Ll0W-J~Kq-3H>8hEhxVxD9y!#zG&3(m3G7d$h{FsoQUVUjGXsR+L5T_#YlQ z257lY2aH@>y2YEG1pz3y@hw0Nzp<6;-6|(!v}J`4-MmCG+pm5X{C2N3kc%5!{K_);-6|b9bW)}%tiItYQ#m9k$UuNNYbq@gw2z5Fs*Em@L`ae6)gs=$`LYfQz^5V0 z(PZvb)vI>N#}+PgMXAFn^giuGg`5V#&!PM6H?5Vp3SxzpE}LY@<3_neP81 z#Q%Xb9Ghfe_7sxP>=i`NJ=HY(#FsRA_O?FjL$4-aF&0TZ;dUyJA=)oiWy!U z(=XMJ&{orEcK`NIX|g&Z+%y;{=-R(+z_sEd6^=``1Oc@(s7(P zLSo*UlG?eeGl4KG6mMNXkWmqJdgrPm6H5#kOA`}YO?9^%YvXp)iFc631MNTB59WdK z3_6w{o8Ncfs`R%Xuh^xCp^%0+z8W^$^&R|ebqxyN9G+L_0Z6-;J^OZ1R{45*y?0>L z_?{orS3l;XOf$P>Z8i;6X~dVpzHhvIR*1<)Yk8&bvkhHCz4} zxG|fgYoO}dam!q$TQ#Fek*@8A^48*+kyvoYymtK|sC~>pZrN5(1;k@7ARd!r_!Dro za?&!)F&E0?RD{|k)kk4LbR82|B2^#u+O8Kqmhe8b&QX8}Ves<+s@Sg=me8{VCj5p) z!wW6TAL?X@=WIGtx5;lhH8$WRp%lrHt1RLd{5-CT*p;4u+qoun-ITUY1I=}k@qoQ^ifzLm5r%UD+ z8Y|$DFtioZPd=>mfIDFN!wz)tHp4@yxBQ-0hm~Hs!Q6?2qsqomge9RN$wKX#Sa8MR zqE?n5bPqliwSuB@95!*tE8BlDf-E~PtRbp2_&EC9clEbP{>z<0e|w_$(>_T%bV{K2 zPf|B@&#zof1J!<+sLY6fMioE_t01yhAOFL{tbR8l|IQr>1hu$;Vfu-PTgg=hvUCAW z!5BR-mBx@6O<94#qsOv%%?dsyLnfZ-2>&I~j)59EUd|4?vXwW%*A~(n12>k{cGnhx z25b|TwTc3b>;+HF#?8no*Qvhx?E|V@;-_h{BWOaSIS$F)#8ZLOUZ_q)gjj$43ApJK28*aJ{*?6FaJk;?r-~JeU}%;7Q~H`Nut45= zB!QGKG8h?RLxHgNbbMc(AE#D8ZFD<1ux-qvje|sgfu;-KY$0vONiAWKlvF8i$8oq% z<43z?1!j20)E3gIY{#a+s}C>A^cQp;8(i^_K5pben2}g66js2>a#1M6l*tg_GxEuY zw=utLH0loX|IRHpi-kHt(5U9AHOwNx)B1%TwIqNd0IgA7K=?W(HJGc;ag@GPkQe?O zH8b_1ghkp=iqEl(uPgy`TSK5OLh?^DzY=T-n)MJHTu~{FHS1x%ygb$3QJIo; z$xBGTtiK#97LxM@l+S@LK(}9UA!VU|T5+;fQbwl{>UDD2OWlDghOLCbw^9<^IW!i$ za<>WfV~=DZawfh=*L@st&j}vn(GVT?)egwxyEQ06gQ14EGE|PU@R@7CD>sLnf%HmQ z?NdOR+il|iKTIoVYpawvN#|EuB-HoC`b_Pkw=L)6+pLS`)y|6wYAZg9IEAkBzbF0n zSp^CLlzVouE^X((yG_>n=7JPM(a%iP8(sAU(z3T!^5vK-P{2I{Ns9YZSVksGTkWE{ zjz`SUKRv1XQ?D10kYKs@jTX||;f=`Gr#0u5u1}Y!1>BK#CdS@?KY?ZDYK@7BrmFz- z4xonMPYrnOeBS2aP=k>A^y>@_rj4#tn4lUj{A5$=LufJuusT%(;FR1AnI9u`1O%)Q z9a-?QZOc2}F^1yiN39nKd}hG@UVo9wWQakEH}iAW+m2{q@zGjLtz!F3y>!w&;xb_g zGF&J3UMro&!#%vv#0qUoM?+EzQ&s&6;R18v0)!bkyeR$3MgrAcc%W_VN(Yor3UF=EFqqP###L6qk72?AnmZgS zzyuQdVpjtW_ipeYEB!o+Qd8xe03i2AVk|AHC&z~HSi7!y15syOaw z3)q5cr9G>=37&aYCmlm_9L{oQcmKmP-RB^TS>w^Z!PWjTb$7881*BL&74fD5XfnE$zd4-HXV$+G09%k?kXZLru%o@CRfR>N z^^4&_NYWH^Kv>JUk-*$k>E2ZAh*UHLy;L~Abm?bQ(xI-Z~ z=1)C6$YXSNAxf3rBO1~LKQ$JOgEo)e=vSJU`S5{dSz!hE<&n(lVZsHbwJMFYT?K%j z%OX3zo6aUtLM9_!z8P5_lE?UVL$|IYprEsC1m!7E`007|Jjch+!DECmZ>uF4WsQ8Y zc8X6hN_zA!O{6TEVW5qjeR20OGwL4}Dz6%H;s_=X>{~rvQ41Dgzw4x9fJG=o6|#i# zkLkzWyr>PFSmJEUtufOu>dZ(eyjN;4RTtB5;%Dr!t|iSD74oftSwe#sAS8CyN*{my ztKhnN(D1(ZB~t!>&$jDQaORT|R9~}4F4Dmrr4@!^;0;;`kiH%?zK-IVu-Oc{51ydIRbaW!>ViJ;1Yc^NxOISL_zmfg>ifs&L zq#y0RD%Ku|J#$1@p`=r+OHGM+d>2j3LJS*DQ+><6ly^s=W;Q>#rp=uJn9gVNA@U@s zD8T94F^H&dx9zZZGm8#uBDl5&2!;>;cTI&Rk|1kFYg&fb_W(6u>xtvQhw&uf!sE%O zz19^JEv49_^{LJ|cjLiPHDYstO_I}lyj;BFcpOy~IHgi6%CUcPDGSDsw1*Ju@wej8Dd(;Aa(6#U&u-ciC2**R+=jR`Mjm;w zFDk7UqJS)Kq@^zQwK0G;z|H$sn*tbgaQFoSy9NhLJAuany|6r@JmP}JgZU{{Ryxh^ z=vqLNTF^sfz0|KfdGTFT60!TNnx+jc-(+>Vq}s`xM^hDu!b5F7u$nGuJY$39Sp?s{ z8`s~?nFv#7c0SZ9^JCVW2z~5Yq5-xGnV+)br&(9^3A&AacWo3n`$-kY=PyHzFCel) zMy3|}aX-((ATr6mdR0BMnvWP4CxwIUd;dgWDW>^Dj3-i$3t3zEO?(BtmVnVSkSt6b&s) zGwA}Wu&yZFeo{x{b++%q&Q)Wu{=p)??M}L zgiFEZu9ZD6bC28F&+Utk!9g1FBZ~Lw`Se#Dd1RPNz%rcp94f=N&^*4byFo%d5^A`b zTP4L#8GF_Ch!2&LR?Lwv^!})SxRPxhO2KLL!xyepmz(c;f7(7yZF)I?OkrP>kQ)3( zg^R(*2r&B8u5{uI{=EvRfa#)4@FR&u^-`Y+q8nK%KCsmvFtp+G#Ng4L_8iNP7qy+` zbKAc>$v-PPmxX}2{6zf1lMums2mQu1xd=)g5BHcitxr+WBG^@G3o0wqnaCAyWxwjQ zD7tGSUUG6saL1>^5|J0kTl%*hRvHokGQ<-*Y{7d>q+P6zVGUGMe;bu^)aDqZaAa(C z76s{37d$bb;P1Kctb04UKU~|^2w&})9|J#pPubk&gW@O3L!D&aSxFJ?4~3sI=Y4ha zR?%;yn_C$Ul@|17w;q{>3ZngI8B- z+E8=I&zgi(EB|>I#LC+u}w2j$UBu&L0?~oWzg$sWn?VKduYuO?ZVA zDAqJnYbp%PBfG!HAk`f`^|f=4?hEa7!&jmMN4G!8o5gB(Zi#40M<+v+a=>lNxNRdB zs{jiax#Pgi%^?9ZlUe{+k9LIX|;=YHyTLWzT4DxDZzaO zl=QFit-fS!pgbc!?T~R$DSTw^kTB z{sVqoVa)0cBmYyXd11v0GTDyMvilAMpbk7$E>YXMiqg5O#N6b`x$C2#tJ>X5GRwvY z#HZT_CARznu?~~k^-yuOqB(&uT~}Sl!GML@uRagoeAXW_BtdNKY#@O`PQG?b2k9OD zk@2SbuvuI~rFl7f5O2|A6`U#-#KDmkO8(*ydPg$U+Vj5C? zQ$Y`$0no&!W9oTfBq68tIyNTShH~`#`wf|Z-p7g#v< z3ij<-E92YJ1l?RbH0m7Refm-WD(SuJZ*@>oItqDG73x^E-D{m^w@)Ug@qS6G(Tu-p zpFh=d79r=*_drDmGARR_E&aUcu5jnjM2t$~A+O@_dqYVprkY5)Rnr+`e?Y?h%z>lq zo+C#N34&q7pST!$RNT4Or?B#sFC3+n-7q5EZg-MPM@u*zXpES?mxsp=S&5kh+{aoQ z?@hQ=cs^>H!KW|(#5<9*|ERSgC)>5n_S<1wzE&FZvS(EZEXvV;paAx}{TjumA;r=x zr7)xTCP;qD$V|!C8`p38)r58BwsiVl4ugN%2kKe6@7ZiMPEhsYu+l1cEfl}R)2UBY z2ta6i%|>g#P?@}OTybBO>uSh(GwzNgu(F&ZcL5BYOkHVRepqnR9e^>4@5)$s<#zK| zoy{%ua;FG?YM#E7(cNlYH+!n_@fe+VVbqIzi#dbr*(atkCGXLmqygdlzT8*%o9C)& z1&K+HmqNDhvZN<~`97Q#W+n>KjU-S$!W|${oY)8K^z}9nwBgkziL~__S4Qr_7Kgn~ z;PZ*72m$)&l!O^ANs)mPXzX_z8S?eBocG8e%kHMUF2Q)DQ1`m}@6JgS1IF{@+bB%u4UQ9XKx8&1>}?5N85t#}GuK@ju+^z1 zqgSo(LghTfg^w9TQ74>b$J^4IQ=d z!uR%FtTOofRC*~Lh7h^CoydT*qRQ>4tih75hpL0w9&t4m_l7XuT>ifP3^Jo*p`So4 zCY}C#C0flVTVK0HL~2ul~xP))2O^aF4gB%=Nh_`)9fTg>*BgRG65Ii?fzZL z?ui%#vX>HM;q8h?qd!G*7lM`>47DMQD+3JE#PL)Pd;@cGn41GSe%w+Z0-ae)XKLNS z+9_`%?jp;kwUmOd}7p7CZK${eOWEZZnO%9ha;43Ajn9z$3f#W=nE@|Y12u?39HFr+Roev4gWq+Mr z7R{^|Mm>D*)i>k)Rb8%L?)^b%(6`E;;Z-w)aA6A(l;ub!^r{b+X0oog-!Owzh_|xZ zonCMV2Pg#{p;?gHTuaWw!?q`#sTL`_n@FQ6LX7u=_8EsIe~neH!jQXtgZ_$>!=Y#U zCZzf_C)g(;3cGdwz5_R2ht-#4H~hPNANL96&`F%}udDTbdMRXtEK0Hbw1Sn%Do0Sx zXG5JkY7Q6*_b93lX^ooOuLM2PSz*zrr0|OIvKL4Je!>SzO27Y>vkV;ilZ9}X8Ma{3 z<>EWaJ7wOYHjMgey4))NVRtVhQ>{LN#)!zEF#(e@4v-5*tu#TqKObom$f#VGTL=0O z2tRG(K>ZvI56LruTL15In@dyLUb`a2I?s6`K-BR=T(cg2h=AF$}{6S;5YbP!im}qZ8+SZ$!P5xcZII) z)9M~K-+7)Or_AM-c~S_Vw-CApVF6T+#}c>2<^dHqtr`SuSn=_I4lN4!@_&BjRM zE|QxLtLP9u7yrVxwG}g=A=3|I6P&2Y|D|6@yjluD+sLZ<+^h%Uz9vW>BLtfP{_!8U zKah}18F%!hb&I0D_O$nxABs&7Evg;J5gu?abgzq~VgvKgJ8c6Q?E%I<@(pcQ#=gf@ zn-g{$O9W=RFM7>buopB)8TccJf1`h>W7&_5;h%qOLqA-z!7PT?{XfxOS|>X7pbS)= z#XS)CB17Z5=HHz=pi!^&=uC~4=X^I_cY9O^{J&|v2I>I z`)HZ^xz9hCgey&W6ypxlE>h;73a=*=va}6@`68=0YYAIWt3mT7s0+MmDh~3pXVr^_ z9IaP<+X)gug852N`9B8G$*-b8$~h__W?lz9*qxL*51b)6t^l?okdB&0RMJ%PNIc+U zy&QhDTeTlzcL`^gColYq$(Rm>USHS?=-**d^?gkeD?~GRfVXg;$H~qs55JEoxHlI*meK7@@=Y!oQBYx51C;~DT6#Ze=k4t^=pffbmz$nH}ZEsy4zn0 zWp$Cq=*J+=?B;`-igWUmozL9-v&;Yl&%-!i{@?4isApw01omn~?#mxmij*QnbW3~k zvqLN=60SA|C4qdpo#XIHCYu@$7(Y#`rr11SqjeJi*OVM9zg-d>+nB7-pE-; z$ORUyU~khiBfTI94SCchFPu*JuoOn7YM?tWB$KsCo{Eh_X(E)o#CFB)_tzVlcq_vS zumTDtPl3_6ucn0FtdI5^xB-dWyAA0EY==Wftn&@2Ktp!zjrML*Q09cSqh*W3U$2dLxwM**r>aiEAyr%?!B$1(u+E#`5t;B zu$MKDAmjf30?h3xdmBsDTx~JaUif(0S(WefQMyrYkdD5t#_mGDxsdE7jsCjMK^zWD-9{3?_Flo+)dbcx!s*=sW&`)cl}q};1&KzhBmSg zKL172(X*=Xr1s6?CyO5X@JA^-a89i%GuG44Jw6Ulu@>{Y575BfhgC*LT>%Hd&!R*W zXeYR@X9d(kSg?W#@(oq}Is->B5aGNU7?Tq!?zx!KqRi`!F;fA$3o+x2#-b6cT{;== zTAGeb5$7Kt5FKZ_p*8-a7E1waU8dCz+}Noz{;ksj|EA!k%thU^Me1@F%;h!r)& zO^?y}xW-`rTcPgmYhQo5A=GY#X8~;4AIw>$SlAmgYY;-574g*%Dvxe-{xJ$yh!pXe zcw>-5ClZJw3}E`^=V{bAvkB!)bZW>&RD zT+DvAqRT>k9~|qK&`sGTq{t0Dvv0P1>J??ys2bLkg}P@9^3scWo3l$I^*{{2M~1+& zIV5Cv$wx%VyjH~%*O9%3D)JwwpdU+EwY#;Pr0cE-@|gEg_~t(jc*gm#dmf4>rHn}} z@dxptiG8<^(&*sIPRu!!W3F*|aWjW)sgA(BFasX|?OP>#W!&Xq!Q*|ST{zdvoS^R^ zAh3Z~?(a8#2#nF{`7y=zghOGOP!TX{@Jt~3zM!wgzTOeK2Ur>E`iI&2+(H(^nFSnNM3ASQ1)pH5O*P_Ot?^(v8$oy4oM$9~U*@_s_WxXHg z>KgVJ#|EcHT?nyt^7cqD_>aP&CvUTNxq4puuNkzA-rHp>hm3~DRqezzBloZ(pj1X!eK#p#gfLtZsoTL9@^cbqP#Nk)JMB<#}^~vwy`Qf zl*;zZ10o!tY*uj%;(&|YuXWvd37MG}hSUC2KphLVt?&c_A1_(@tey?!?j&7gLh}6j zl2CUH%B$f^&+yNhP39$`niWsS1D0u@wB+G9-=LX;T}v7D5burC*HiE>T;|Qb+!;<315Bk2-K8iC_tmnbFyaA)Aa%_;6TKfBpdj0)Q8F)bJf-Icqyi;n-4*ngDIq}JZ|-w>W4H6A|x9RlD>cnA340iQb{ z4l!y~BRe1Vi1gBC(f--(^KU8$+y7SaNPs`wdB|a1Sx?PV;YRQ*Gqn82=gZIY1k|ec zB~?CR=ARRl2xtQ1U`6N5E@`G_&=y?x%f?RiC?fu-!1 z6-yU&7T6;uyqtHj(9U@Zo@Nfw5(dzg^8nGK7A?DGxi6FblmVwb9vSi-q~ac>_1rvd zAGXg0ZO1??S%@WfkLfy5 z&TR9+v|tPF^o@?S5N5AH_DBt;Dl_gt{mO#SGICW8VR=)h8oAsYH0CGgEa^ZSf9=JS zxp--Mv-ym(R`f07%c{nPvhk@ZghsyMk9w~ zXCYFnJnl|=B-t6;ne(Bf`A<6XEy|XL(pgl;3Wi!o=9V`b63DZXH^p@i4mhp1qQLLk zmND8Iv<0*Ag5ew+Lv%5)$evLH7J8LTdiuYdRwTK~Lk3Y3PI7rcYTrH*RB$(vsHbI* zK5zSm6-MiOT7$^qVxi3~`3}3<7O!66ls=ZFy>LrMRBLXJoVtsq%>GTf<`&TxHziK~*OyQQpv`GMKOo zyT^6DBXVuC-dN%Ys8q*&VIZ<@X&^%{>_Cik?pN4HcYlYx}G zRVZ)vFyAK<)`Ku^sux|5Oi4{%l9<3VjFrxH*$X%#ae9rSWo=n|j^r`WCXtumt6+3p zUXhnp@hV@hn9v->F6Z(&s9?E6Ibj+(n;4$xfL~FN)|b%=7LsjF;fO@8XN_CtzE1ZS z*G(B5JQ-s=0s{eU->X5^Zu5k3mnAF4_ejz1G|TGik_sHRm zLfR3n(Y0H9!$^C33v5(ps8*E@H`d>M*!;FeGoB{Uly8UM?wmR;l9aL{dp4=G8@EmV z+2}DrbZdrFoI_&AU*r*)WTB_+2i<3xue-m`UO&}yV9GTdji%zzN+)2(z}O>6{>WZx zl|8%x9ic(E=J>Mr06mQ6G%t1PKq$!*yd3o{0mN@{?}f12(;r-y`9JyzSW7SUo^YRY z*9?VY1*Y$>CBb3Hh%!~7r0r9VN~avo{*2FM)Ood;{qe>5;~ZE?Pd=h8p?=61x#4T2 zpOl^=82hROlJU`KHHUmu^|;X0@07q_;)0pVL%iBdk?y1k_B$yvrQ07;xZOhw=wnw> z#g!kiJN{ZRBWXE=uTj6`&m6W!dZ~Hv^`z|_jB<`;J)xHHku)lQE$GBv8J(8wHyG-$ z_&m!FZeQ%}8U024DWUsUY@1{-Q~YsS-^K*y(fkr_)HrVSAyg441Ubmu~u)X2_+8I;i6LkM=EA8H(q00#>xHKc?3 z&vv$r#)2KW##RE;i!p7R4zlL31X)2B<^z9Bv+t27)_3|+JA!5&T@c;0t~W)BR@Hf; z!Qkx}dJVHaqovrSiHYRTn1w)`R=IrXubq!StE^ZsOEyzW4&^yJc=sOK4u)z+^Ng-= zFHf#Kuz2`cj*P=8pIsDc!4JzJ_2%ph=S)p25$t4f7nA6e+Df#mRga$Yv7+8kru!92 z{iqJJ!t`gxQc)8#t5;Z1rHovbbaxB7q`o@SDUH}=fsW$98OvjE8iwOo2`FM0lyJF7 z=I4$ShMfY#L16wx%=hbG+bRu1Iah4-M;@+6+7>j^nI`YsXwt+9AC{D{M^oF5sQP}; zc>q6;xy?u(?J!&-nJ|kCf8h3UqVbF#15 zeB!B<M#|F+KaaQf1{nLFGG0tkoIMX(};|O{U?31ADS<1jm5x6O|{_Q3IT6XdPl> zmk`*M9@g{vRi=&rC@)*f`Ah5PmSru8OW9_5#BMv2BE{3sh{a}cW>BZ-Fopp-ZP%$g z-!+30)#l;t&#A~1lH?>PSBFr)A!Q-tr173w@T>Gl%lIA3SD#jjIP>Pq?od~Wu55m0 z%QGwe*Mf_w@|FJ6=3VE{-ti<)OB+hLT%xoirO=@y>@HmTyclr<8t~ZZLMQ_3$sL_d z&Wo=t`vLzu5cW8Z-@IrGCx=XMrd3B8#CX_a;jFa}9eTq5Mltj0&}MMI^Gn;-yg5e~ z!h5&dCOn$yXjla4YPl5;$YW|s-lVA{mvV<4GcT>N@os)d&r$|+G8Xrpx3&fOE>E90 z%m?f~x^~f!IKmjuXi6p{dc3s&v%iNXx9DB;>)7^EUUi|Dk#sBGv2izqSLuQFRH0>U zDmh2ZsoGrXD%%ir-oOeaen0P_LB<}(notC(yIqkjoQx1H1e?zs=Z+V*6r1LJ(lH_k zS}Wt7f?RAD>aw>u%cmsMIhMvRzM7`pP2NGs7-7}!^n8+EZr!`j@s@_^j)DX0AamNL zz=1vUdNhaJoSiU=^05*9QhCA8JA^MI7!!XkuVquxR%nlgsa46&w2Wgd$kFu)v7FV8 zS{T{4Rga%(Ub;VK9pK}U)DmiZe1BNoc|nRll4F}A>%nLqgBK-(v}hLTYgY;WbB5;; zMf(E%Y;hG>X&qjlLUOjRCgwl9$cONUKSqv}xHRFK%ccV3 z?y}3OgdI%AF&()p+xX2b%o8&f&D@znq!pp%rVrJrRnpRvpH3#?YZC7!YHM1S+;X3g z8g30h(Dj^-=6{ZNJ(uqnquVN6`9p5*n_&9A44%Je^FMLr^_DAvnFc3!rQR(X!Tu^A zqs~){mlpfUr9I2R`&fUB7eBLJ&uuo^c-RBxszc5_^Bx}7-jOzAd@W9R`m^1>u-ZGC z7*tUFUUGtUAK2-P*%)ylp<&*!Yy(=N1-VBcro`~lkO-d9g_bQpSA#DE0$jpE-qF%5 ztdayg4mqzyc3~C~GU>@wcl7dfUZjLyw8&;%+v;%hkY6(MXH<8AJ)V5$~5C!NzH`i!5p@qA6c$G{^m98Vk=UtRc0bp9ZM zWzFeuKQ49dyE)f77CVuLLlD=`M<0dR*mmJNIHm#!J8scB5FKM@qhEJWnxFSq?^-pY|vo( zYL7t<(zbFk1-r)8Uqa^TkIQY5d zhKKnyKYS~?Ka1zKV$PkuO<)W`@2AAQjhM%Aww6>pLr{-fa(&ZaC^NXwe6vcb`S22E z!Ycm1*YYuQrgx-jp|z5r_8$UZb6cg()8|R`oeFj6V;nnES{`%ijSmXzX{pPq$HQp; z@Jc^?%;lw=6e=^xe3M*I(Y(EEE@`$w7^pM-nVtqN*(N}ZRqhZ1DWap1&!=G@XR^vj zBDj6iZU|#e?2=nX`1OYW;wW+YrPN=_=;Q8q*{fxz;7_JHCAitEtPy-PZlTd3WvTd< zj{X+&4vWARu#$(9IOd&1fyw!!(pNjxcrzjBcik!ZAr#7*pYaW+(9_6LYth;V_L~(S ziEtV+qYaQG4!YLT|L|-vBu^`)S+GuzcDXS{q}&mA5eldR7Djxkb%OxYJYz283Vvtr zK?duWztPFbhg%T)$vPpyT4fo5?CuuBj6evc!d!-!EsSMFvUXO3fZa?-$44hJBU`>1 zC^2}5TLHOHLu1}WTn=zb`twi@r_!e$lB= zsjs}FvQFj*S40)cf_wLjYVzGWHoe&!K^R`C^bbwwZO%6_lX5PkLL z-Q}PAf&>cJ&M_B8N6k~e={XHy{?79miiyET6QoHi@vgQ(`}; zZT|_6FlHBiGA4A2QS7rQ2lQcds;n2gCE4uwb4M(*-Wq*0Q$(VcyJ4WmVa;JlB%_2% zWzQVbA3BwRqTNTz2)Gy^3z_;}`1wlwS(}nHV_Y-7^TIcB^K_B<)W5IUn&2YbhXr~f z={hQJ@JVLk6$wjvDYcujcuJ7u@6vJ(dl*L!Bgl0ewKK&Lr&dRmsiA24MS7(?GS$7u z%^~)*>-yw{NiPa~L`JvZ`bo4nzW0g!r6=B=M*clh{mvY-G1_|CvCdoWuItY0rbU+! z8p9GeB|0tdeD8M@+DUKF|2fH!CN^4F-KCmFlv%K}JgjDgfiCG;MrYg;n!b+6^B1jARQZ93TS2K5TdbCb9 zrg+wl(j~IMb)%E#bXlAzQMlirz?!`L8T){+!#tNmD38!*%I;{?Hcj1{ZK*vtnz|p> zaA{lnvA(o1^09eW5=7>7(k&b0C>ZgD`}w(aQ=QN4^5rszm}58i;m5_$C1B>u@ntyo zhcZK22!>MMH>om(v{gSdo-+Dz+9<_G^*;U7`TO=OkB6yOqHEyA#A#`$aWkQ#4vG&$ zSl&f(5R82(?s;dyR>PAM@O~;hu?4&K|QV zk<0o=@UIULQxsFnx#$&`@W0&BT9!|*+*Pyf(4ap}gnfAz2ljr2iX@2MyydM-Y+rW4 zg*;-!j>c{ho4DQ}7U~(ZPSRU+xF^LX<;KSFbSEJD7w1ntc9?@Sh-QT=-ipy%Cg)gk zHK8<|RWtH_!|v|}x7>O4m1jDWMh9l}AKpMxf+An>$ce*((`iZA18uz#QgM#Z&%nTk zKa7T|*$Fk_ZO=u;-QxMs1zYRvWGrIxJz)b6<;>gj}$c9?`Egq38Y~iGTney1f;<7W^1| zxFZ~0GJ0^RV|U~&`Oa+)MBZF(?km+P4ivX`4}Njd5Jfmfe51aMowHD$ea~dLt7AS; zJXR=_fwcU@_I~y#D2HHpkoZE(Y-pCdFr3txV$`jDg2sv@1D|GyX1>h3Ro&zCJH(O7 zQ8n2*{!*;DN21>E#g~nACkcL-zeTg2DrKP{`xh+F+9gROsY}b?YkNIrBqp+d78l_jr$Zc zG#Na@qov0J!@JddGdF1N?qmyEH$5G+9FLyn7bB{5xQ~GKAhm`+n-Dxb^cSTs4}`S0 zfLu-%OOcOdgqw|J+o|hL-=$qG)^ol7L&}~0Iz*IC@5*n4B6I9s(0!G)Y|b*(Ufpe$ z$M6HA$IdFNqt6-R;DF%)#o8M0&UmM)$c+Gr2c3_;^WO6*0uB5E=vO@Cx zht~pFz+iNIIU3d4Gj8V4JPjFACzGY=ke1<_vZrjR!RE3G-s3 z_0?Jus0Rdva)6O4F}wq^&aOPUFf)f}dYWJQv`Hi2S^iAxnQQNBI`ztqmGe`0lgLXw ztPc0#${KS8=g%9NztPYu0tgBJJ#ag|WQ(+zcWc=K%3rm0V6Uj1+V&^sco0ea^9#GJ zt-oCV4kXtngL6ETO#X%V-06M5NQ|Stnb7v~t!mXP&AIvDxQjjlVsRX4RG6dq3oKJ~ z1Ut{I$R+5(LunLK!F|ky=Q!_ZimG`VRvyNG&6P6>XK>HlIz?M;B0efM$BTBHb98e0 zkGh6l-P%``D<-GQlTs~fOZ5h6!u38fuaf9IRX`^Z5e%w3{0%PhO3_Lv9l2%YZj=g#CI{|_(we59rD{J`fn zcckj;AVct)ztGMd9ADY;)WnkJ88=vuNQ7a_U5rK4F%CIoRIL(e~FTa_Yl2)>g*B{({~S-DcG z$M^1YOvoMXsVJc_?V8|of~l{IU5#26<0 z)VVCyabg>+14@?ujw@;1=3*=Lo!ad*T((Wh-X`Pv53h)d;@Rd|(2wR+&Cly>V*_(d zrHbr%5C4)Lk76T1wh76vQlu!-64P1csShF~)AHLk4tsn7uuO>?#)vh1y?4q-`*MTv z(^g54CZ7Q53X|`ykQ)cQ*la6az3oDWuF8j*7~Dx>!XU;+K4JLFenREDQN%IQ6Orh+ zydAd%;~|ATHu%H|7_I7XFk{u5wjvdxOtee6n#HYpHF%rS;EA>(eiSWN9zC&%A_O}x zJL4O=Aa627VEzv}mRf8mPPMe?bS*qn7?+?@%dMTpD7g3gSJIn36Hw)7b3uEjI~#f# z6HJ|ZxxuVId06)C&@T3#-^f#R-*D|lPZnMhGW8xDIj z3t=W=@wg6}4Lz(h?>c*?}gt&a_ z=b?d`FozgTsa~{*T@Qg{3sp!4=aG*fA&wVJ(T$}&=WgNmGwOhIkwY`nbRnzsI>TKw z8dayeE@dg(v+}hqb7L(GJbhU80gfhg{7$097?BS2A>2PDoy?JO=s&#IX!i-vFp!3S zi~0{4cb6O`Ms<*e%6q|Vz6|e#l$keq12nL{Td#T}sW!UMNr(CCSd2ekr45#YdlLmY1>cPzFEfjNa7G2>q=m zrm0@5ef?R>yHIouBWifpUWWcKox^@8vW$TDWPNH(#e9uVchIsF>ai8ASC7yns)-e4 zyG+<;>Mo>+jzpArz?AOlcm3O{Ei^CmT0E&LWL)z^=U<<2Rgb@hiPsN zqo>vrJ~rq0pJ=^{BKaaX)kkjUSpW#HfLkgp3iROQi&XQA&ddN(hV?A#A|tln_OwBqXF6p`^MAB1nk75ho%o z{`UF)uIu>&p65E}Ij?ih{krd4%NxSQVR19frNIoR+*8Y7R~<2UZLO65RS|3~bil96 z$?|5jqZ8^6{RdJ9dA`jT=rAX~Uc6b)Tj(qTj{&{&v?K*m1qTH`vQVL;X4cVS@}&Pi95Ulqz?`HdPQ@_5=p=$mkX-et`=L}^rB<`fDHL}PS`U; zpcr?>#o^?9K$7K)1CCCG!8Z-C~$bdz5Q zMWgd~+uo|=PAvuYaO{XCPu+tAqy;v20=ux`zr_;Tktsqm&m9ih(F@fnC4iy~|Jl_n zK)`9gK)Q({Lvelhcp6*Jlm#}4Y3eq&vUCS?W;O15#0K|Uq^$UikL_-Mn6u`S1%D>F zORu!OdLU$McNTFN?*x5C-n9BJ(j7Eq^P4n$OvGrbIcWw2%er1pZ*Xl=Ykn@sT!~9* zhHZTYz0dQk%uX=;ZPQgG&Xc%7An~%T>i;!FlQ5AtN52f(_0FqkyV~4)IDfzJ54&<@ z97!e9>%^F2AmJqS*n}>w5PDqR=wm-x?vSD(le=#3_Y1HDC%?A~o66gBh>oF!wM2%4 zX^Y+x=GWA%va+&>($Xk4%n)LJ+!jhzB(2R8$o&e>I*Pn&EYd&cq;xz5cXEH4SPZd% zrUaSVq`b9LD-d@-A`C&Ci39~)7=P?BF!;GCo3y1x5$9ho0j{?XJ$ACgRTlTuRoYX? zqXsg$)kSd@oJ+!uQaPj8#(oo910|w%>&5G(zqZvx9cnCVhHFNj;r)w*k+nNYoG$`k z%z||ukACs_XhcxbZgnv6P(?5avP2<8qr_PcC^(&5@)50INYLhORy<&E?<7HGfFUk%P`j^q5x5q1Fd``F%JaccztDhV6O zW~C!JcAt1aahtwhwSsZ7?SHr4e6yx{XTm?H>TqI}dzxcetT;w@>Djxd%h$YOmGvQ) zX5sS@PB(bn>2=K31MR}4x0P415s);Z)MQbq*dMMElw@4$mY0_ZYJdU2#H-cVu?HtD ze6r-dh0E^WRqmbT%^hG2&NHYhtNO_@t8#|P^>;JHCi;Pt!#uNhj`&$j54Zzt?z6?j z07O8s(4AYu8{-L7?&TuTtOmn{c93_gd{9X8lS9Jh*r#VB9bWT63)(EtPC#}hTvx8G zjO>?@$Jk1);Yx-mI?IBk_^fKPiQI{r8S=zusbwhD{$SQF*>K`6fXZf8{@wV!)~yl+ zd9TMGTpAN4@uA+!ENcic$BSPi6$Dze`>DrpCkR0-6_2`GqH&q*jrr77s=oZw=!$LR zN~39*JYw;utC8@WRm5J_%P=?cDxVxu?FSTL`2F8G+Q*i(!HUqw2e)aN=&8F?X4yo@ z>V1PQ_w%RKL6V)Lj^@q#!EBE!CTmnq(M193H**nw3Z0{XLaBhRihYajAw~KjB|KKC zd!;C^-2905-JbMR^vK(hiIF!o4<4j4zL793pM@0E&|B31J=SMq#}>{|kLE1%gcy1q zA03iT>>-)N5Yj`E4PuTV!A=Y0S5gTeX(1BNqe#%E-imCV(;OAS6GZ+!0M2FeT>);P zm)C=oB>R80ActP@nJwAPvNg*z(pIi#fX#t|S%dtqo*ukzWq+=EK0j=D|L~p*SUipO zn_t8tgW3UsK4J~y_7Toxh<892A!ZQ7HN(M=9KVcqqLXfNRbBG@Ito*@s(b`;CNui? z#dcJLn670Fml6SWomdb_BcVi|xo=}K7>P(M=dimK<)u3l?b*3vh0hvoGtl@s%2Z|C zVnzQ>yx~@Q?*4e*tEdYNdrZ?12%mYdk-E6_oeUR01!HGi#!}y}Oo63lnNia9?D5|L zPosqXowK+#AuW|1F1=mSd?My(eXd-i^F7Z^+7RT3N2_-9_p&-}TL%si1XqGb zeV8iBI64!M#@VZlPk0xLr^460=+@0~%Xleu5>9>i4IJ?*t2cGxIm?YAugD@3yO}qy zxbTwae#eomWnYO(n0ut1^glQLYEJD!2ZIA@@AGdv@SBf&qjW1VKwQas3Uh?xe6tbq z5qN2S`$oC=#M}jonFF52VE7RZX zC4pK<(~}z_?1}X|*dJas@4pPdwSX61Iyp*hVX^aaeCj2NmEGYWv#yjXLV-@Tuk21T zbR9FBA$@JoPS&yM79-Z$SK&zG93S7?x)IjY^IesZn&r-cqk>sGl!<7~$A&%{wv2eR zLrR@wSsEnCh-^V*ZJSXr(@j($!n&0OO2Ou9DQVGa_eZ!4LuPl?QMxq=UR`ghYzz1e zx6zJlFQf{Q+=MWTCm$GWN?&-%O7Do~<45828OC&#qzBMI5Tc0yJ{{Ex1EUv-<*Z&J zk~xXGW}Ja+w;f5(BseWfn`Qo-@$F|1{5zK%;z)jFz$cb2>j)5&4n%S=o*GqH1(VZi zjx>M?O@w^9$U|_3z`UOISUy0kSp#hqkO%rY&|-r*8%EHh^RA< zovSPcTu2kJZ-8y^b3bby{oEYVb@u?a`qr){+B4nmMW`X~+Q5a1fmI!Mdb71x5#IZ( z<%H+FQwrUj_|m@0H6zG19Z{QIEMtu!^fIB#NuL1T;7}CHg`g=O>t4hoa5?i6tM;g~ zFIhHn>t~~Xj?BkB&6J;w4<7QfNlA5xk&FQ2S@w%-v<*TaP(zt6ICIi+UfP$h3mi0J zWvJj1OjmfnYOatjd&P&K$(8cF!}P}EYc6aSaLdKIb|leqlELc>)>M7TK@D!j$oI~h z%V*#I6~;q5{${&PS(+~mUYta=*65$otS9uT%#31s+u7L0yd03;fJhq>>;&SJk8R$T zul@Orh?%V>Q&->^9P4`J21r#L9lRiV!2qE?TOz{r!fPK-ByAOqtx$9cgF|T?qW8@RQ$LO|F6nipXtGJjXaUG1&(kmHYWxT?D>_|@wP7n}Mnah-x#IsT| zcm!%$9`1C$X7t<>=MwWM(w*&i7%k9YXf>e2NRH&L)3$X1zSt5Tg|sI~ud^H`I#{CP z_v&Z8U`Uc9xyw0s`&6kWl@_bei zGIxGUeO{Fkkh~#P=8W?3+UhCdP^@`90N~mYZ}^5W?Q+#0+PdwWm332hBw8R>ChHAT zKy^$SGB5a%uL0<4lrMZ8o@J-5$>skG@yipwj?Iqq}h@57s*wih8 z^UWg$6k251mQ2-hK9VU-v=v=7?!}d$CNVjm^Sz5ppSXrUR>>74(6Ibv1VNGBxf;1A zNzW=9d}ORhPd_U53SMGpcl2sm4^5;#6!3hDo>h{B+@uOnWiViFQQ<^-DUIl81g-M%2l= zWtnm~rB)GtSn*BO_X`sE`VF1P0l&``E-%8I_k5eZ8!JBvnbJ&n^%We7__02Bjx^&U!44jkpJic%VHE5Rp?^gz5~8KO1?tF^J{!gja`mQJGKVIpk4lHx6ttLTV}kY} z6O8wO`{kMj&Dc%bSdI>;KLLtf2V){J~nFzBl5%e*4m-%tvrKkq1HjQNkBC)vw6Qf z?3PJa=K&vjmSSs8y|(`4@n|rawDn*Hv&z@Yuq%Ks%lHQ9QrTgBohaj4iJ=jKOg7#O zyTYFu3be+wTeS7^4Oj#=swnv1Ht$>ez+I5iJ^1sK6zXWZ)ziA<{}6!=v{tBM8nslewQ1ElWA#mWf<^R zDcA8@z!?WJb?`liv&R@qs+N4|1 zPrOX}vObErO<>#d|4-Ex(l#uo04xS)LA|m0j}E`>k0niLokZ~H7fAhsO{RY0n@!C z&zQV1itF7=bYyB2ab)%-;<>5!yj-N0r7;*#9e*4=D~%StsnYPNSlhO$GpWhXW* zSQ=Xr?HvX!Vk2T-auk1!r=aZ8D9^2f%(pG?SBEJnt!DpMxj2xMV_sAX4>fN=aE+u# zn;4HR&gi{K3y{Axx=5>v3Z|jVEibtMR15RekySa51LyK^Mp-a1HKQjd?4JY&S7pE<5(aTrpIq_|QBbxqi{iFZP#AjYt zy5xOEL}~-W#uG{&g<9M_cI63kQYC*Zw`;%j0m9A=+r60o2ZRbvzX!DIgCUv@&ySFMuI*W|NOp2Xm>E7o=(=ggZ8NX7A}qP&8^j!1snfoe$g}9@M^(8^ye0DNm_uF zHO+AiuBwHiZK92I2n1HNFjr!k@wy+%*iOv!Q$S>3)pIvckfP~}Aa{bt#cE~q3;WQU zwXc3;Wkfmg3;YmXSIenAGo8k&jX!$0ZgpoqGb1PLAkov*m3z|RbAW5r>rg~qL82Su zmm@~7BXyu@2Q=|4L13iQDl?=P5wNC8Wa`!2L*U&#Ty0vgl#Tr7r{GJtUM^27%J4z| zr<=VYu^w^pr_*QJbICuV|FSRfv$zb^pi=s93f)ZAF0T!AN?lE7l_J%+9qr8b?go}i zSBrPh%kreAi1exL)g(#6bvx1g6hSfI6xq)3b7zWoVCsKk0XUV*WE|-{ck5+P9Jyed z0^Vhf)f*TTNzs0>?u3&?iJ7P*9^&rLUa1O3_8Tu?{5v5mbHF&mfd!{CQ zVz*n=$?hp`Y)A2pHDr1T=N+-zR3{&+z^8yYi71X));zpPIIL;?GGGlJLGyG&_dLVU zgXxg_y$ll9ASUWa*}hgQ%a#}(aRO(LQmzuJ-S}UdyRWY9!Xzu>Ei*8%`+&!jhi+~E z)?NXmW-a|e;Uq@l6Kt57?GxC&9B<>iU$Gwd+j=(@v66F~(F_F%Ca_qmPGjYxq8MBi98`muG_O!@Yz7+>E<8!}X{UE4%Tybp%i|e}JXLmF#W@ z9~XaoppYu>AM?T*Xp~h z28;LURZ`>l@IAJtBK!MkE$4XRDA`CtDr&KhGX7ECqnj=4K=yBiO6z?rixjvzcxCUY>%_p@2O8;iwf4rSM5} zuB#glX|5I*)$3V-{z=f;I_pA+Gn zWHdeA^mTkT8?}Ig3~&-%HFq>8St}WlpcY8-=kb{-oc!%V=iTYTU(WZyFKHgL&rWJq!`Z_=lB*Op+{*GT!@&Wg1 z2%&w6_D)UDnMi_;Wm*wdkHWnI2f-D5ZKbt-PgS?c*H<96N-votwxD&%%jn{Msvs-? zXjy-8N`&~h3@Oz(zar#Z&_>FGl&nxZba4m;EDTikdX70w=SxXs0G2Kivl zyVBIx{=_@Ho~neUD*QWFjEL`j$zu&)-I|5AWjcNqgbMYI*hh3$zIAB{XqNFAUhF?J zeoRv+GzQlmO18zO!8Z6%VtHatna1~c7T)dlrPex1LQsT3P)8cqN=ZQgvFA*9rX@}p-b?@n1luQd&fl_l72qBq#f}!I+05wQ3`{7TRO@2fhlWQO zo2R}Ek+TlTPw;`Z!WP81AAQHX{^GLjHe3#u1T(VgxfBe!F*Vk6Rzf}nm@wF2q-Pc1 zWLXkn>Kt!VcNL~Rv~6sy+=O!rf_|)<_)>C(&gVduuUvF+yBBPlmS$q{BG(+3!CPBFsiCU9B6)dH<N{l$XS$Ji@E9b zme_BJyf~%5Ii1>Rk)|fdZY#nVQ9R==*bS|Wui77wHQoQVtP&nynmf77gqL@D6|hq7 zY#~^GB)oD%PbIg8j-pv*8!L(^bUN(A`i5%4Rd|&ihRD_Uj&3~3A({z(q)N}Z4rx21 z4FL`n+{PB_Zx`2Zc?|>3`Llb}Zeo&&7IHK}avq_X9PSW&a%bH2?UtW5|0MV1-sdif zDM1HaccS4kGjnh zwE0XBAhyNaeT~eeX7#|zF2&GbKntc@&j{*02uFx>4Z3ZZErsaYtbn{_dBnDzfi^NS zO(g)h#}h|$j{hy?`EoBB9jFkyA%C+ap*IEk|H}t;#x@VUIr@WvQ}=Gd_1BVEvr&iq z5#z+QajgjzsM;Y=mM)}^bXCy)n}SdrQ^5Tf`eZ4HroNaH1;S^qGUwG!R&g-ix;1iD z--Cnq>XkaZ^Y1?Pg4Mkqv4$AXh60q*uvhW)$+c7G$da zKp7>Nfx0bQ?JSsh`;RYOoz5f=mnkFazi_2CkWtUTG8a54f!G12BchFiV>qdwHb?hd zE?uswe%h^ZmGXu7H~DoYz_|`DCMN8itLj*Sx!_X(ZD(op2Ua^zYmk zFO!<}xK!ePf88BCz_78%?5Zxu>}CDtAP=rqUXYGaE(7sa$fdB9p8@pK+|IByMk7>? zG0#fzT~}X_pjE)JDx||$n5w;dee3Z~if_OI&%3vf5|zHXaZbZ8d5b#T9c98!u_aa1 z0Y<@trk^smG;m%6#597mZo14Xs&f){1+O?oKs@*Rky18GlN`(|k? zr9yYJl_dsClqy-~Q|l8jl|*2v6%;4;>?ONf-EyPm_m}LT_HBgtUZ`KkW!#afE8ieR zz(1HF8(>g8tI$%OOkgl!8U1~iRg;i+g{B9v1l<7Sp&oQ|nn_LYKqbBq*vBp?owrh& zxu**bV%kYfi-3E2JpG#dXOM$xivz82*Ld z!Unsp&z?6HjQ^M#^nkB8oM@iR*ssT@Fj3yW;Xb-6b4&K5f#hU}1Zzh|G@MUtOeds4*9uy_S zaop)DFFq7ofu%3_I4|m%fSYu3P7>6|vMyX4Kh%P=6|w6Xp(w6|)&+N)-v(oZnkduy;B;Lj&z7ib z%Z;|0B~&Ym@i&H^e#H7{{bpv%$(5LkcgDltgdwc}(_w1R3)1xrW8hhrMd_YXHZwh~ z3ljQ>t3AERH*IxpQqV+J!II_)#bI0aRlJ1J98^|$a)SH51X@EZ$7hNq6&;Z(=-9#8 z(^4+nSu%d(CrGN~0+%jbmPGdP4d!fzQI+f~3wZJYO=I(>;L|;i5NM5m{Btz(F2x>0q4~1TdW)6{05r2H|8gO!qp2=IuFzqW&`JQM zO!TB>TIkgG*Ri&7`{zy2N7g(Q$VY3YjNNvhkd0v@J3aFP)s^S^jO0`ezE_6E z=qTW}eSpuQVQX5ry1G)s^FaYwnVYFh>k6TYtrsdYea;4<48kgJ&CdH?_hwXt4bR2llZh ztyo;Kp3p~$kmD?)` ze-2@WMXQy?ZKV%AikFj)pP5>{l*^@@Qb^cNzDvKO4T(;DBgD3GS-Z0j=)z#e&vh0h z4l*1veVuMwn4V>}&lEL?+E)eXjZ+hR6o@xh2?KF;y2#WZu%M9RbeBPLm~Y6OH0)lm z-V=QL6fd~$!qVOvfFE4kvX|3T1N+;Tv#tBB^j5RfgG4y}U5B&Ox+HG~%0xoD1Bq66 zNQfP^rIkvgrMG|M5BLwcNPdN1#SZf*L5ey&-;J{j8ZuY{)Q^#Q6+Y-j&qY&oI&m)6 z-FH5|`;mY`fCZ+sdV+bSz!b}&U!7vpQ@T1cr4TvQuiKa8V}7rpaP*^&>lni|oTlW1 zW2Ol!d%;0rU*iYEAN7R5=fd2jc?)fJgc#bv?sP+`Kz`rO@ySWCZzt@R5~8c0wzMD( zMaYz`t_cw&IwZBpZp9a_ZPIYeLA%Vg8JC)ryyFBbn9`vkDSeZxQ$6_QzR@ngdu~R~+C?BwW07_O9U;nsOeyOuAuY_hsl}(9| zrK8NF&Pk7}DjP}C30-_<8pF8_;II(gg|g;S{w{_Jxtn*xzSKZ`B-TG93j5A!D0kC96 z#(2NQa!)KEcdjVQ>tuOsvmvj?;yf@@(gqW#pEEqU_Irz2{ni58UaQwut_K%kfRe7k zvVbLBsC6!Z^Mxi=>N)TmhSh!glBvaHS zPq#ehb%xe4=^OvA6NV68%@EOhx8_UpYX21g5*|P zJXM!Lb3`mnpH;B-2v0CX#^R#HTDDeix+M9{8bAS+!OHd^ zQ37b_Yt4Kp9S_H*C;WQk?>$gzvhsIT^K}w}3R{bG5lj*(bjnR_ z1w{$(M|+wxOH@>1_s$F%m9pP93xfHFQzE^nTv3$r0Brb7ooN;5YxOR2_A9iSk3-#2 z^qJy@h-=AHYs0&WI@0*_ab7=LB>KbGPm%p^_Ed3nCg05;?aMbDi>zce#(3!PsTB&#kr{YsY~V)9BE`u5K?E7AuDO4 z)0p+wOXKy7m&o1}b9Qaj8;~y6>;Uy115qCey_B#6AlVc`?w{qIWlED%44yLR5Um?c zcTETdO6W3Kl17%zbJ)|cb?>6f%z=Cat;JvuKYux!)c*L?b(@&_-6G|5Py<1pTJr)T zr~MZB?_BK$A3%Wfs?sQn)osPD#Vvek4S&Hltan+2s8D;2=Q#-&5ldc`gyKs#Q6ET-fW`A&+ zY=|K0VQV>-i@>=eCRLxTn=3D3H3#rA z72_N=-rc}pmq!4FR(Q-HKMd`0{z;k(C`|fLk2aFKlOdV7DE4>T9yx zmqX*M>3Q%6Hv-()yFPD|%3&r*)B!}sp)*yM$mpD|rtX}wHnRig87f*mkhc>HzFc)V zES}aYie_EP%Hn;fHEU7sjrR(+z;+_d)SHU)BICj_V6H_yRMj)Q`~xik*>&f zfjaT(?QgdactY3yTT#EVxI}~YQd8YqmpOg)J!c%jfw^D0MAAwj@OGHP5v28p@uyDC z^Wl=~v3{@_pX)20tRtGF(H077I+p+TgEtr6^zRhB1!Hy6KF7dhra982TFeN+G68fV zi27&&q`_T=zx275RI}x^>YQGs@vZwd;5&t)R7E#lg--$P5JDXmA4U5*W ze90Wzn1D6nwD7OGFzCFsJyE|Yo&4Hqb5JpAMSCWQ9GGZRxMI- z4qxsLi)>+aiN!4y3&Fl-Kz=ZRWV1c5OBl9@29uv%6BGuPWD`Ev32r}CDwUnjXL$2U zZ$AV68QF7v`;r;%>cDi@rPWYkE#wj5eO?r_KtU>LCqpI}8+45W_>~vNz12Q#x`DlH z6RBQcPpKh&!D(UmB%}sScW659|Qc;4f}10)V8_0E!6&Q~WyId7FgM%7W|L3xF% ziu5Ko;}<7}LdTC9d@aE`^q*Z-85dfYdJQCsO%Iv;rE@t_t1iE*bL~FD6Zs=0SwaM9 z#tZ)KW*&YSxGuYD?d6{klUCR)9tQEiC=e%((iXd$TOHyB(QjUM<@ZG!!SABS08pGP z?|lYjpJ{^tzF=L}=Iw&9a0G%YV9J_=&YSbsWP8c(i0U&? z)?O4hcX?5(hB88g^@Bu`EW$=@`z+>O&K?O$jW_2V7~2{9>!IJlhcjxG;SXy5&W?VD zg9sZilT)U$!mQz6(nEXeXSrf>e{nj=wy(1XBz|mze-u99@zLgTn+tp`Tzs}h_`*UH z-7hJN5``3N@?});1~KHqe6gfNaHe*X=<<y*Fc~N4byMFsu-3z9R=RR6YQVf0e zwR^ViKSbtcH+`hJ4X!JkITq}&m4Q6~^YOFO%}?1qc%18mzvsEXCZivBi=0C9J$-HT zQv`!JNN^p9Cr`L&97u`I$(Bs?+4J1R(!BS9f6oUen2d0}$_HaTbH0s{qjoH+Y=+w( zhi;?OG(@TD*~s#nuAI6sw{Xwfzlu} zw29CW9DRA3EzI=AiXGreg>1DLwQM>;S!;>1j2wrIcX1lQ@|Ky$@J`;aSC@GP#;g3t z<+M-}#oLhGXwlSLe1>5?Sw7!s;xtfT4!PK^Ka8{6A%wFd0~V~;@o4rirCE7?Cc8m> z4n2vh>h{9oGQ#m$nmz!o!eV|^!yu1cb?<$P+lTCON5~SIFz|7lU0GD-W|8BNKqks)Xz5LzQ)nj;>E5-SQOIP<_Cz(ase-14e|`ml1WbYF%ao zErPbX;ND4lbBiYR{)sBclHoaD)3{}4J)Aj;|6Oq?v_Wk?%;*DDMn%=|diBLiUvQou zc)&zkg>|Q|+-3)zQtw396+jF*q&Jke*xWp7X0psBcJa;_2wclC@I<8L4!?x$8;ETc zKlPr^C{NY*K!l18H<2{NW(Sw}FApsHe5&fUSuh?pOH)U;j~oeIT*}Ea|3tsL9%%$F z9;o*VgX@SZ}g;rGQ{Bz%xqF()*<143I#zWpn6<0l4N&g0 z;s@H8T>5yW2{?1fweUQ*@Pfrzlw}}KVPetKr8_@&a=x98o>@^p{6Pq~*-QR>@G?AU z-^EaCul%cg+}Ca8u@k!F57?!mvz72=Vn=mp@gUBba>XRP-5KT8V{~q^tt?4I{ z-~UKR*>kQl2=pIIj>+F4QNzdCOx@Q*JKvx$jdhWQ={GWUe;%M61oTb}^BRIH2-@aK zy;0yA=N6kaf9Qg{mzm>R;BH}Y(DDE%LVMvMYCWL(8au@y)5p&h@MM%hVIRETUBvN= zYh;_3a2*$4S-cXX4?YsF8};}(+d2CIY--2*r)x0)`WC~}<1<8=a+j(2ch0bR59O3! z1yWACwwUhxSS@8O((#ygCEEk-HK?&dj@yKh3xcQ@eE^-qktFn7tm$d0#E=S0Cf|-ll=0u68b9~ ze3*ZpX8Ro&^v-W^aJx=VfUXaYWMZCu7_N{@w@CqAEU6+FObGoPC#S#$Ued_bNIUb_ zd8P$Tm)}X$bh4Z&a1|MJfZI~JR2W^Td~0Wo{%Xe#l&7KhR&)4YhaIZ2+&ST39_R30 zBt_-$t@1p$#CuJ+HrEM}ma@T?sj$*zXe;o@;~~0}AivFM2JE0A%}yuBf42B|NhbF& zfe9j2aIVoPhtQbZ9X<3+jx8JJ70SEKPINDqz7PRGOb73>2PsOMM%hW5WmwcWI@>b} zZJu#{X5a~Y=oInnFG%zlY$*$^a+|X41|H4B;N$C`Y4NFpqOk*!W2zRUHP2hPu0X(` z?oRTXD|JKs{D*&;%I+KTX80$t-R8P%ob^$5-^yNMQ0%h%qyOj$=KkU2@#iPHJ(6X7 zNmZ;1+@Ie(T!CK20Em*8!YL@hISurk==np~SbJzm&Ny(bz!PZ5@By?b zIWFsEC?7b08e3H0*9p5>LNe;)C3K} z8SF2MAN0QXnJFr`|Hs!!T6}W<$=v;ntX-=ymzB-qvf=0xVEu2=T`|{}9HcDLycg$- z$l*rgk2jCq7oyGoool}XR_>d+*gJ`Jxt?(2d3H;$@TrL3|5DBELEMAa!PLyQbj3s& zg7b{ku6KsLE;-8^hckKU{qWXPZl;DH?ow3f@{k*gg}WcM`G~4Bt%a# z;n|%NE>!T&BSNcSk*i&3V7CVUK2$_=X^fvFKXD;+D+ss1%WtP=0F^m=_b5J547r~Y z(pZg+`SN-Natzd;z=zUU_Up;UVzJo`~rxv0Yw#os^RbZ4CDjYh$Q z2{;33-U6``q9Oy#kMIoNfB)rM?VEyTu#vN}1hFPkpTOrcmJyF;*uBitK6kLaUbc|q zznTy2{PKwRgnzS09dPme8X+Tru8+lb|Bj#|Q4inZF(^$Hk;-^e;bVe!)MSNhXUB5- z_NJZ-8!ysiW>)HK6)IBskS{W(m*M9$OM+D40b4D@Qw|6SkWVXKsD`46W1gG=ixsp$eb@JMR z$E8ET69$6YdU+M|U6axA;or{~cw2KaVXXZlll~KzOLng>_X-;u^M5;gO<8a~OE}W^ z&{O_Sh^I?ftz&^m(~(m{@(8@_*Ge4v7SycL;WWo#HC9mIg$Lp@rM_TN>eDn8U_yY_ z{8w#!cqObb^%=}ZXz3PZN-x6BJh6d*6_6L(4RhPnteL{l^%@Ik4Mu-a1^|-&r}J%; z1IPp~H3w0NHj`o!cE?%C7`Le~%RcJ=!tNwcn)b7xs}OP!bO-)T-C*1wsiObpbLuNL`K~Oc57iz3Z^`lEnH6_~TJk&qFPvJwS@^+uz0m zhlw$#YEy&@L;I2foFvr)!Ty2fBa{_p^aRQEn)CEk1p)i&H}8{XtK&NOGdECS`Nu$a zGQqZaQDCx}LWx(_#N%DunUcV5a34p3#3j;O-%&#SoM{Wf!c>C22q#n(8_f163} z0qr{}zp7u_X#Q@J^_k6~ODt{}#y_s;wJePrrmJ#KwZc{i#7Pzbpz4)pwB+l4?b-pS zp6C7_NoN_>g-$eXlf{t2Ba=(&$AWVi}u?pioe<<*ouGTspWVcYdksV}75_X)3P2y%KbZ~Y6- zbiX>}WmuPVZ6w!b0#!k$jdttt#_r`--*GY{Yh=)!sl3lgR^evf9vpj;o<*IN`EQF{ ziwgg)GES$a&2An9q=*t#xx+U~**kW-{Bn?wL7^L1C9f~tGMgXn9;R)qE=2l+e+&D4 z9sMv){!U6r@pkR=y&;pDFpMm;*PmHEbUvj~jnC7_;9q>cbui}X(HJY4b?o>%-Gk3N5oz1#qR+-JB#!MEm7Fa?Uzz#Ty7+;?$LAjqEv zKjYE1kmUOph!Dh!@UxYBYn=jD^Hh9yRs{urs(snN*y*)O|r2$2rMD*ls028F;()v8@;L--<%n%#r5JU-eoE- zraOdz6Ult_SD6h&XY3MS*`~`y+cG^eSbvuH;QB|0Vs@$SMm8NO2s5wO=&w%)Gnbfb z@%K5lY;T$eV$&W0-8!&0+POcHd;Mi z3f^cfR!4t>lHFo^4K{V4F-)`20tbLxF(4O{J4sX!Hf)r|-?_OhW4Uf zVP}p&wVhQf#B9IezZ8(HwsdGq-^c)Fy{^_JmIwV#!3%Sn6(6{HD|_HK02qji@OS9q zybV6#6?%u#`NOW##Lk5VNaW;8Gqo zil^CfjHBEbdmkqm43hs6dyOcnk|@Y}r^jAQPIo zIB=m&-A;Irfe^}oMxri*2>JLEPfTR#qo=!I-b&j1oQBk$H1@)k4DAS%`KZP;`(YUw z)|Bu{s3I}P|1!Uy%4kg`k|?Dgice-o`+u7W*e=X_({GfjMdTLckJFDe>g}M1or2BY z8?c48GbCvR`SVlI%1W?Rcd)G*C}zArvn)6(P%GH@b0zNY0b{tTr|i50XVky7oie#9 zJbh4_>;ouOv$LQ*^FdqS8{;9-?`hZ@k^qNhB|ovoTW`Hee=4(gO~G~fEgE}S+;3Nw zJsHm8;aj+=ek+MG%bB2!eAz`4>6DF(q4^FMZ(^v3fPRrF$+vAhz>0U5gL3^(Cd5Wnexdxhe;|pmk0q%=t+KFWkwmmb?MYRmr4h4+HszdOvS6& z66z&Al}_{GpS^V&ip_59OKwA5hjLovfEnCe{A!{;fS8Q3>9xUbzj(SLPVnL{{XBW| z+3iAKuy=UoKNSY=NxVlL7u^5l!%SxK;Pwb)2!{Mfo%zbEtNBPpRL=3E1rmOhH<)$T z1>DCHPtnQQWj%UEO|JJGKCvWk6^fx1KxrO1UFK0{$#tg5McDnVdM|-sC8Z)-*W^F;m zwwnGBPyI%b zCWxi*gk_C0ODVe5ztrmNPPolJ8}v@K)VW&q3wF&zgt;?A`OCqb6Ntm+SXE{RD*kC| z#Rtl=gJP?{1b-3A!NN>4wAdTNKd@645`|flJ<;2@;5UQMZ%)Z2$_HxRj}?erGF&l% zyp7Io7F83r>4=Ca2z?zfjfoze?r_Soq9V`F2vwIw-jIN}A(}$rbdqWxBFFk%y%mPj z6z2~{5X-M)CRDU~TiU2$LVf`lE=OweRD2MN)I@e#Iv_#YY7X%I?+s~?Tkv{kE&8*0 zA1_Uj-Dp$z+m2Tsx2;d0M+|NAC$o8YD{8k=^C4EKlO7G?ht(gHZxK{$m~s>J#kb(k zu;rlG$@Mo}66MR-SxwJ}FZ@V~Y=-0;&JE)k zsoJcPP9fv~SLdVdI6i_(3ybK_acFbgVnwDsNiiXgkxm8cE6H_T-EiFI{b$eUgNv*^ z%iS!bp{p{C);yx0Wg(4-?kIu(j7KN57yi~gh;JuO9A0k^lo$!4vmVNp3nI1kKdx7Kl6kQXfjR$?KUoF}%9^GaxdnrRl zouJBz*5iZouhi^X^h8d}pHkjCvdl#$lW%;tU1daVlCu@zWNkNZK7bF^-K2%;zRdD4 zF|Tie$w%snbaA4TE8X&c#hO>j+)$bGlj^;lWWrePgK?r;!D+Vp+(=SsNEE}nNP23j ziKSV}&a!7xQ}@9_t5XGgWKiiuqg%^7G*-cP*iBgsp~>ZfC^2DEYBlWl2zv~;U%^I` z(zM+(O?`Uw#Nga!Kgnw2!i*@b+JXpLM22@fg+q_8n3KsdnV8h+Yl^MM?e-P#SYerUZuRvQytTJ%EF7{r%)!dr-dUkF8w^v`Ilaad#jszuMHR1<3WM3*@6j<}8RbnjzLnzz@5bJ2FTCNFsc zkVFdUfX`{7T2_sKpAGx!@AJwYcd8J}Kqq`L32K+QAg@iE*CCO73mtr+TMR`fV^n5; zQq=#k&KVXJ6M??Z_~orG4L`)+?)uvT5&LzNPRS@U;V};8m1RgCHu|<$q}05|_wrXn zqt|wg{}uGzdctkqm^2>dJQGPS#o)DASGoa$@kqW4%}yjZ19PI*|E$i|E=BP!(zRrk zeIwFjh(;4tW+OJk=E4eUg5!ixRd|cBh)5cZ82SA>zF=Bj?qw`a)SD@8xaM-rxcAUp<&IPeY3Ny zKB#T_zgGpX$*995$#=fGiMnZy+CIHXGyG!RtDTkH1bc1^^?*rt3bN`=cug`jy6QDD z0tpv$?!)L~U+uG#WS@rs5;S0}Nq3hS`n7-;V6}8ijRYX63odW>8?Agu+NP=K^@B|m zK%b_J5xt#952u?@j}3MgUPt=;Goi{TSsgG=Q*}R{rfECzdNitBxcZSsv+Kx+39a8p zrD+p|g&6c4tJA1hyHH&_?LOFan)miFR05(afn%Q?kyk!)E<7GY6toG4d{|*el`BoHX{n zM@cdjfv~@|v;}%UayJv%|3_|F0-ZM-6)vt;=f;^F$x)=EJJM<*yw^mHm*WeM5e`w~ zDrQ8>f&kTm!W9ka#SbNe^BA3*w3*!-2|$~f$akEc8+aMlqx8}QR2{EVEzWu0k~50M z5X9}0@mBi=8X@8;!tTo6(jQXwJqgOo0-+I15l5B2v_#qT=Zk(^nRnj#;Q!GuY>bgzFkd*XqC(|QbF&JqE9=i$y_Lo zucnr@01q7Y&zT)Pb>1iE)CR5`sVKhVbznVF$b$kud)Mtg(SCa5C6|oT`U=p~y~y(N z^R)t+J{!Xa>FuuHR?KF{`CkmJt8A?q?mobRcdGQuU=5D78BTUQwJ?}FrV777`6_Ug zJ1U8+E&peF$L8&yE=s%!Y}zxbcYv-hz@{Z{XiMMXQ{?`M%)Q^pt)fcMfQ84%!V8-W z=%sxBTxIb;7ZDm|EYqSnh|j;dHQ`VJx2cBr=(b?*Wn9-oAZ?SWwDc#&KM}YG1HO(1 zo$*oUQvN^+)l@O)DF}9rSW@`O$Vd=<9?}H)?*?d+KKFwASL|4skE-r=8`r)i!ZPnD z{kC^<7F>Q%D7;MB`M_)CKwD)rFSrK9A0#p@2bR!iW!O0q2_ULGZ(vy?P8a@wNK4Z^ z04X8qzMw>gSQ;NT_hzK-h6R>BdthgDq9QaWMfjD?Vwfce1!}Pv7T#S=P7V*Lokn%*Ag5?U?onQc$~sil$1aO|A+;p=XG8N3+i zA}0^B=~&u+R&bz~P868UbiU9(u{xZ3&%Dg|lV>Dadn|ZLd6%d*q$ug(iTonB5DUbm z%yS{+brp;%$9e1AcAd2O+%6ZGV+2W z&q7020Td#$1Zxd!;R(8Z#Ih`zGKb#@l<@3eNqp_G9@9vh6q`^^=c`7##!6D+QAP^G zBs0?q&h!+kIyGsDd6fOO2^nqmSB1;NOBrL^9za(e1TX8MTtj?ye}LMNj1Sh>C?uXS z;M?t)^mi-RfXC=-I)COp_VAJF%=n__6*&uof~j_GJVFTIHX>cv{*>HqO#4`>Dl3jF zYz(|`!fHzaXM8@gkBQDt7x%R_%|RF77SaPrQbYg$LF*_~2-QzXV667~pFjQr@}h(! z>;mWCb`%wflynYA=DZ)8D$QqsH4I*KZr`8PxoI?OCv2m$(y6V~G&gTtGOWf`9bkpV!Pr^e74lJ#joyZ&1tfaW~C~zUyq%Rhhqf==;C>!EfH3Y zfp0D*7DMa0`$cEW;}%Z1`A*C?bH@2PP+D&oeHmBv)`RGH{t5XC!6QK;`3~-L8^SY! zu9u9OIzE&1ekb8xU1!}PoDEer7#X8}*MpQHdd_|MTpMj~ls zu6;f1eER9@qeFY~pD?%Mmv?{pK{vgkr8RLu0ymih)4?QpC;U`1*7Nw|!tt4Q-O_eN zwhc8wzm!^}_+_+IDBAK*{Z-uh^dVcEJEgCj7(G{5RW*OfykWyHoM*Jh4? zD$98yyde@CfDLaU@+nzi>Jk*0&)M_x0Ls7PQs zsVIu|T`pk`*%3(;2}|wGHed87Q{FO>XjVF6(=8hzymh4f-wht|#xvEGLtPhDN|FMM zL|WR6uf8f4L0!&2XmVElh{_mz^UpD>CWh97|S9_ zXW$~BVzn>*_)WzE&FyLJGec+GGAuG#`Opurx*_^-WyCF;TjxRrrV)E9oY}`Fog^2X zqn@jzgXxNuNM|ECtDd!`>bI~sp5)p4N^Bg=4)9xJ+mYUoH- z@VR|NchVVNl{Qh$rFT53$f8T8|KNYpcUl}4%j|`8&N#4S&M`|tjxnci{O=3@I)*6Ac#-y$ z3E>V0&pBV}4e6?V3Tr0|7dn)ve&0taXNxAn_2*Cd7kVx2Wyyc(jIwGPbxh~rQv;5q zDTefg{=|vGJI7Tze*c8|`RGZNXV}H?xYSy7;ly_40O2;S1#F%fI~uGKNjv&qbl{-I z)*_6hSZ`s+jy#!tQ5boAX1HvlW_H_xpN|H`GEqL%PSP&L^rP3Y2qHF&hj+YOge)}t zLAF$(ahL!RI5^qm4752>Xu0K=CkyOSa;29%kvP=bO~^Ytvh`uNu>Lf6s*nsFxMZ!_ zt~J-Da^;}}`z+cXu2+9}a_>vs5}!k0b!%|0pQik`Eis34`ah+Y)%GBxCK^Gj7MF)7 zYH?Zu^D861+yeKe{EFgp5WKjLwRDkULFRq3CM=)gztU`p%J)4urN2VVFP6(Ni};27 z9!(_wZa8!^NuI<;Abmz{1s>u_tObT%OZ)#a7T>_j(3Tkx{v(#8oAd++^M*?l-v(Is z=tv~$+EjP^$PWfOFm4Sv<*@v;h>|J!^VgsGPZS`JBqgj95k|jIx9BgXS!EJ)#?{89 zshT=@a$6pA35i-$OKOw1=)JY+nLbe8Byz*M&-;b_*14u3CXD_aJLo zw^cu9fp1H&r)ik54Q+tJZ3P-p3cGue?uQSNK!J%EMmD(ZE{uY5F6X8`^&p78qAoI? zmxx$`uil2Q9%!XSr`75G>dM4Fv0B%D`7~AvJPNGy3k_4)HaQT5K_G@6Ugi!7c~gcH zD8f~ituQv##C<}9+e5$Ay5KdH#MI{3J-W+m{c@R*AOVW+<30+pegkzTQKu}EBCGF` zZ{9JfL739?1R6#1eLH~%ixGurBDo^ji=9EdvwRNEWbtlN`+Ul5;rS`j`6+xpHPJL~ z!s}Lb!zKie(279DW>8i&0(yfp1K%qm_&hmdFY**EQ$GZ% z3pd=&yJSX-XzRWrUyNqXiF9LajZX{ZN92_y1?lkHr(?&{MBj0!%68&mp+^N=yL>+i zTG?^hg@rYr3k^GrT64X>w~vs5qTAdH<%_NQm*TB7t*#X`Li8g&G|s6C(OOc!E& z?2&s@s|)aEz+Zp~tI$U+VO(;1U%^zKFyX<4xIHy$-_LissXel;#<-Ree;3wMB4yD_ zZ;Z&M)xMDlv?AL{V8K0wu?n1M%7^@f{TyE1AKvEcQfDtm{Oy*7JfYxBmJh0$<+!MG zX#WYVeT@Nreq*n^$^;vg?v_?htD-co%AMY$G2lKP%=kbZy))7Ra;se+&|HezGh63d7jlH zw3B8d5%0iUKQdN{3V8=_3Li3vZnVP}{}nKYPap&#o=SQur``#5v*XO8i9=F7pAIvF ztx#f>^WFxQwXv79pdgJa>!or?V3~OW-JUkN2Qi;{RWx5Cf&h_l{yJqYccz7|Xno}i z_a27x!z@a^K0d5?@H;2?O=`H=!4$T{I}Jil>i#iK*7TA#+^oL^jC%~yv?l6(h1e@!+esRK#Z z)g&X9OC5}5;E~Y!Q@A-_JCNs^wi$Q3wIl7)hM7*AvL>ukIDzJ}<=yWj14*MXWxq)F zHc^%>O0dFB+H^y(qi zHsRBY4W}%72cDJA9R=Z=F6km_re`#aHbnV{oeZl;Qh1f*VK9WQ%0J$6_;FD#+gtWY z;A_|)KIX#pgod9?X87!IM#a15-uJiQB=NJTX6$Pl?3o&5|9KgUvD}8ePPeIp@;`N0 z__RH^J%y2~F}St=ANxh-{GHpCL;A0PO5@g6=(90+p1q+uYOjRJj3bOVx?= z1_&REhB38H3E@lf-$qla-HMR9h!j`h{rd3uAnpER?V8a>o^t+Aq@l?+Y8ja80CLUn zGg^E;U6Hg^B5BW9Y^6NGrkTA!?Ojqzst)n<%UnpZwUwJ6d-6g8FmyqbySL zV#>ywq902ly;5c^bc^=dk;iTG$q9MAISfx|@VQ~+oWRogx`@_{@j!b_Zj5(4afg+! z=DIMewCNPsu&$BTW95B+dBbv)ZNO@yu_ZTW28VWZX^|SM8u=AzyM%K!gT_12zRyH( zRu_EJn>Mk^t9H8ZI=9hok}q)mQ!w`b*L~mfJ(onURizj{|HRa=w#lW_9iJqFH*^2p zAXnfu?+#Jr%+?FiMy`R*b%Zx_Y0Y?z`e&)96FpK(4gZib!7DoXp|Tx1ExB9QvzUNqDNrEpO@At`yxLJZH@8F_ho3t`^ zIo=Qz%p{_Xot3%)CFV)2Z!yOnh)`IqI%jVVGkHRtz7Np=o+T<`l{TZEPg7CU7nq%; z<0TQq)j=FTxfA;+OP_P`wUOxB2ZqZhj|+|$VF3rp=#D>8@e{DEx4tdEu+DjXOz1b? zAg@Zec~208Y6NW%l0D2jmFlHSd-1p3iE<_r$qQb)jUJ-8e&=}k_^d#L&Ubn)mIgV6 zO7u_s6{iCEAC`lM4T%c|!a7abEte#cpWLk{;@sh9+P!Fk4FO63`4Kri%%lF4b@Mv` zS)ZQ%6UC}flY2EsI_GrphfWV0Ts8*_4nNBqn{linj7X_D8TE0azmu(=Xuue&#Cis12&f#l^D*Gd#ix}YD$Qk=)x4|y~S2v&1&4a3x9c7!{3qf9HM7h zA}7SpI1Az!UFO~^V7&43A`c2-DcT0vb;;g5FlnxL=zs9L5hGP{z_g-h-ww3*kLdm5 z6r*=pCNfN=5Bvho|H4#MktX{gBk^-}>MUm-yPfabg2FP{(rgvhchLUG=7`>1ROZNP z0=xbPITw#Zrd)XRWp?qRmODj*I?GG;HmD9gX_{ogp(->_6!9(05BTW2XtQ{bigGhXXrm!{mRVQfmGbU6c@;U!i(A1}R;FSycKaM@oJgs!Rn6bVouC7FXqreGe_ z^DJzRp=Khxw(mqah`0O7^;-j61>De&K$uscHN=-Lcmi|z$J}V?bwR9M3*}V`^A2}Y zbtkEBxf1o%=^dl4^}`Ex>!(wmJ6S!K{&ilZWp`OW7N^~NUe%5I_DqKcRdHm6 zKAz4KzM5?4E0mX&#=MV!m@z0RM07?wZ(xLJEmN}sx3+`ZCoii*JrimFjX|}2Ng$~m z;u%i@!;>nYKy|Wt^hX6k(q59`j_}gka~+oHi8+U+tqH5&iOwUKjryY&7zfpMky)}C3T=wmGqhjkax_KbMMx4^gptb($~O(Zm-{^xIXaEG=k-VWmbq}89YH3vp^}e4C49&WbHnqf4RF-N}#)?%6Ti#uLJ@%gNBhWohEf^kE*C56H-Nd zY&Z@@PE?0kg7A;R2N20!Qym{JSMX;3YhOkg@FxeZ*-aW9RdZ(xEsu%`b|UY6GxUXN zv((ILD;3M=C$Ot3-aK`DV9hK1jx&6?w_uf1z^Z}$egTnXOcD0S%ucT55latns(_Wk ztblCd^J?d=V#R^)okVdqA&n>G)bvA;WZwm|LwJ zHr}19>>`Yymb+U)G`=o7$}-f=<1ACB3~q@Sr*E6dCZ+Pb-weI^-v4*Q;7PwkEYL-z-pt z^$|~jwj#t#IijWpUrOCV9$zwIp;MmAqwU|luYcEAumvkT$)+TAikvo zCXMdGv|2paa8Nfx4+ew1Jnu}y6W;dW45|20jd!$Zeam$zHs!Kx^lr;gpJje=_Qi{0 zcke+f6d{ zbMD&fnzg1y%ZXT$U=jX+pxacw)pXCeNKR;d>`nPV}r=PANfpUcL)~tk^G$M*~k*_Jn`y z|93+Szu!*1C&$?y!VWG>2Wxt5jfk0tI!p1ZbU^<_LQ)%iPlim~w2e)dIF4BRNC-Sw zDyPnBz*^~#Fb~=jmj7-zlra;3x1MEwU7W5D%+NApKcJ8vlNO(6d_G>75BhR3{@;yC z;~HalU1$du4i4c)aj`1JT z4^Tb>utw+8Db(Nja|X)nRUWAIi{ml;NoAc$d}x|z^A_-cwEx?sYBZT2VRA|P2XWHr zdtJM_>l}TT@0G2{=M9#aQt8i%zL$F6*`7tom`-Ab;h!4aY4m!3%6E0l1MkU3`EVXX zx|-2_vOcomi9c-;Eqxx#C zBUig19n3zq(q}`)jYsG*bNQo3ot(x|xVwl^+Ojitwg(d~`;$53|J}$(XS-r35nwTQ zt9(lhMGJy|Ob|AigD5Ms9B%XbVx<^y{CTVxmY@QF>>Te$cI*@Bz{A8Xs;v@`gOYY0~_ zpdj)Udq=Gv1AseIvPodHYS(6jhXkaWfFb6;^cP!?H13=jJ>O{}gMd^e&(J!Kysfl~ zjWQGu(}e7*m%ZmVVVfDka-tL@?^d+xAu;U4OqOp3O1t8(jLs2VrBM0b34|P76+Lhpg zVuyj&U4@DI{(nHH*9<5oi?nCYok>&Lb)(@0O>^U1)dx1N2J7)G*`L-*UJ!y8hL(~% zzL}@Da^~e-up$ICRp*)q4$Sq-aghvKgD;)g#e@^a$s7XyQTn;#E&F<1Y2x4?q-2gn z)X}k@%%C6FkO#SCuh6>yVZ@Ifz5i~!dOwyHDWWeeVjd^36nL#in%Wi?w@y9G+P)s- z(F-@29jz}`gi4@bYQE4Hsk?D}7Yx51;6vV4M@BHSsoml8%^TdPh-@*j!&$?`Mx#(d zr16&Jk8h6(Ml{Nl5q(J@m-lXZw<|6ve`%KPO-cw|7MeFyGd!>km%b#+ZW6X2|k1CA!B}4bh8W>QBd2!pY;1#H8J$29M{G#aK0*tqBWBs*6FCKKl+<8go`}b6S zf)(q1rVqM1ZgTGJq$oe-Z>{+TXh}!}{=E`;p*?GGofIASAoZ4h-^p@^eK5yl@PzWkCW;&?n7H0bMMr#LhZeocWzSI4w! z$MIf$+oUr{39jes;Zd=}w~B%rb^x!Z1tQ2&Ob!ihe;aZ4i*4kb5WmA$nUl2y+`df3 zcnB%n%3-+RNg+R+Q8(RrHe2Pk(QjE##i=3+){7Ka9mH5 zh?~>F{*xuW58@O6$fZ)!wBYUT+B)wWGyKKj;0LjFevYi{L9NWpwe4X`<5vuornh)B zC&Uf^yU}sc=vC5z%0QN8E6cB9FQ~p=v%qSLdIZKK$RNkE9?LGT54yMR8D-d3@~p*k zx`d?o#UW9BpmPh$uZJQt5ezwR+*my2N@M1z9d`goMSGsWboAc@SUAh4`zz$Gon^Le zujwo}bmV>#Y@OJeANV2Ka<^JzR%^y)q!Qdhm zTukeb>BHRvB{6Gt2@|UA+RyxVi@uE>YSL98mZ8N$2&r>{gS z*V;IWux$gU^%nTdL{j!fh**i65h*G#Zb&pdNj)Uwa^EU2Bu8sk^W{6-jx#9Bavz?Z zAa9%=$~0-yw?i12)Y>B#lNI+uu#&NCNcSY&Z0{23(4HH)5M}>c@b-Mc;405vvx-W- zG|**O!CY}{NenIeX5}EffnQCQ^wLsP(kXEXdpGH<*Oy*a<&tOy)OqyAc#sa2@6m8X zXxb$J`jV;6Sm(z4o*33r$|@vmjJ;Us4X``5dzNDRf+xpEWTq=i1;LI^S+>Wx_0OW=B-9yT_{r2fy>81)JP69b(ra7 z?wq13-N5m^8i>kgdW(Mc{FolE$2l(ihc8EL0S5{6K0sM`TRmi9H_~>3nJ9#>raail z_4@^v^1ug$+?@?le6I-o{F7&|tiuW=Wg3S{Zrnh}=}3MMFa8pr7yHkKVRe%g+z~D~ zO{+sL%2K`fKXUQ8*y1>8&cG`vmFI7INxji0`Qw*X`vG-J$dJH?e7)hx^(<>56LcjH zgfV*@;Cr-g8FRfvOL_5Cib~3&IjTvCQa1PL?wdcJmPwk;-Rcy>{C7hxD8{Wz{-KYr zFd@ln=mRHfI`yhr=Rp3Jm|TUaug`YvUJ*XQtPfj^pZ}3{F1rnEu(aclHmI$eSO@ac zpzG57L`FV52WPI80>WbfHCp!zj`wiB$GukOLnyKD|MOOo3Ddt2lvd^$x@fvLuXr!$G0W=uw6N@J)z^;DPN&X5O+!8u6-3x#y3csBET$%IIC}gd zW1^PK9EJnRR8yq}%cxkb%!BSth(}uTqgqkVDTPSxItmvbKJX28pTwNdTai!I(zw68 zdiVNQPS6JdQs;jQYDK=x=Rvj`{&5+2Z3|0L4}d7=jD#Ke90DWLj-rj?(>hq*lSG^f zXe_%J|G)!bb3dX&RxCz3l`=uZ@W=YD=$x6TplmInn!A^I;>s?!ISC*mB{Q{Kyc z!@w)9CcQ*$G-RJ~N%;;Jps_gPx2o+Y+D^8F#gl6x`b&~NsyhHZkB`V&O^xxT+2iqQ zyo!Bs;d5DdUs_{EKnffE1M27-cu$4fYgKvC37KFc_nH~5j(OlrFUM($c$%qg_7ett zfxpa(s#%V9PBVLMg2wobcH+(s3`ix|n3oX6!i8u}8uiz)7OJ!}Z=TC6arIWpqFha)^)Lh|rgtJ42&l(`4F;j!F!r`>C#MgRK@n~AirLcq_PY_0;sThNvws309jT z=wTr?)*&j3T9*^`LXzBXABHl(9krj%fi?E;S;=BOBGmdI2NR>JT!f^}mlt1;q8t2( zo(t@UDqoIG^o@Fa4KAs51sl8cg3WddKsp(@2A1e9dwq38jHCRG@V$>|vk~-NwAO@`bmi?IdRHEr$J8Q@{g(}#PX z-Hjd;mWgW{u>D~6QX7V%n2DgkK>c`%ZOe(==GWhIm(bts!SX8HBeS6YZbWGTzBT*u z7JK-<(K=C!5v|vz6O$XnJo2`~NH9%E%d>1kdb-#x;JtkJSX4IH?y%e`k7pmLZNAW- zP%ftEooh%g2LMK1+|RV#WHD&+r+XYZyGwGL^s2_1I5u_MaefT(k+tO)8 z{pf>&Si;YgM)gzRr95*j`v9;*xrbsmNJpd}(P&kY#$EONS*RDi3FEYc6>F1)Xa zt^gmN-|J<(7vdG#8zUD}(Mep%FG#U#Ui6Yg!}%KbU(SrF_F~B0$=EK_7+dGQjXPTh zx_!KXt6IiksDiOZ(R)P8!dhzKI{8&o3?HAR4t!UEQE+V^yp+f1+;GA^lwqZgA1Ot5 zeR4DW^x|L`zPg<~TYGj$f5yKbT~l;_+RkNosM z9%<*8SJ5a)c{##f>&;?z3d)tClt3rXJ|787*Kz1;GOn-J1vu#(I*9SJ_i`F?^@Ed4 zH1TXgVodrUw|eVtDpl*3Wd((U%mc_+8An>|30#zEthZ2_3$==1aGdB0G+^w%8)P%w z!-Bq*js==gP{t%hhhIXb(ng(3F{gF5PO>SJ{HL9-D28Z@df#4B+Y|}xM?K63#n)zh zZp^Kz4&wRPIOJS7orxqoyS#?@Z|>}HEiQ}b-G41;%IO6+Abj@imEeb$n}7QUYgYLF zgkpY+BA;r*M{wK*1}JD8pv!p3DTWS8)~P#AWM@n|_0kVlbpCbzl#^4Z@Xb2twJPi( zdq}Z4!a`}e&-1nK8d+YZm|P@PLY&D{{ozxeI;=2!o69yICaK- zB+p&K>!!WHk}QQ{?8tj#e*pfzb>=fLZ}GaB5bd-v3gbo_Y_T7xA}-R1bOd}&DhvIo+&X(YFJJ?p!Gm$SI_PrtE${W7 z6q*Tkk@l1#|IaeZ?N>KUTqd)p$rc=5tfzWCGYP*uR2T0h!> z%XWOabPu)KPj647$K)PRiQ(StnoC+!yz5h`$tcFw!vi1`;;gs$1T&0J4C=^}*`6~A z`IM)uQn0EamX>QIy(BLB9^Wt}Ij^~RGRBtusV~z0^^KRs9IDshJB&+!?2ugQJr#G# z^ev*df=1tHD@$0zvv~&BP7{${RXr7?U9p}CTZzP>zRnx#caWo&AhUA;0M3$nlp-sR zzPO*qV0dJw=J8vqYGWM7gqYe^gg7w7hGM zNq~=6B^waV_tdX2`Bmf)c{&|4d6Ga;%G{<2Qb>Hivwj^?m!trKdk*pu-q=~Y%(i#F zm;=S|Xr^T7y{g%hN?@E8zPg`Vc1D)ZBDUx=r`u-m!8@(Xz!q(>aGtK9<2_-RmfG}) z!CHi1oE1TS5OoUh^=e+&x%YPf6wLJ69qM7F$9*gJ+}X zWp-tx0=4x!1n0@abW7f&CMp1MS4&^3!jj;Bz8HyP_4bI!^oy+aF5~^dCCRqPH2L4m zA>}U4Uz{OcqautGTf4l4cl|q#jWP9D^Xr>q-I2<;jifM~N9{kDK;FU}7~~4j^5lxg zk~;DX+2Jr;R%Q7(-KoWUXEw!HYEo+o&xf?gN|-J>R0--a{a_X`^b4Ptw9cN4c9pU8 z%`Au(l!6}=es(*_phaBYJe}f8-PEZS)frvHFc4Z_zq>Xg%>TkhSJAw5gd=3Nr;VV@ zanhfVE6aChhU{q?Uhx6YaG(dpU7OX4-}Xl0%03&$qR+;Ci$EgG2V-|CTN4>A{bGFq z?@Vqq$l>axSFYK2mR{v{;zC zA|9E~w8ht{9!=A<9xvG_C0>jErP0G7(u*{OSWM&*1V-~(i>k{hdvvB?Ay$B4Nf_a}k* zwv>Y70}*Gou32n!_@B|mYo;cpkZZ*h4KTVQ9fQ~}5-==jt+~5DhoKU)CxBk}->v?M zKAinQ;lRdH_rj7Nm$l7JTUqCNo1Q8Ru5>vabmW%Vc};*ZCRh~S&mZ%9T3iB%Fhvc9 zj&d~rQ)UaCg%{BlGn25XiU8Xr!T)Zwdb6zP5;~id0C&&pxu+^yRePb;Dq{uQ}9o+|iZ!xLN~h2*!R(L=qI z+GK2g1j!_qsqRotj!~15a+I}QK48q1KbacA_O33f29UdS{!nW80s3x2WAPl}hBy;h zk{P1Iw1eCSn?zkujObNwc}9OVx8lhct9Mw z*-sX4r30&AdoQ++HO@5bcN5m9ekakdXN9Gd`Qxf@T1z;3OW2hD<2!WRi5i_yY=|(8 z)RE-xj;BCh>I=7{^OX`X)X_dE-X))@k6cj0|GO&$GFw{2QRei=T`Y3WTXTlV@QpL@ z;y)4VwT5)lx8`Q26y6(_j*fT5GGi;^RyBsHbgQO)a8{^tmV8@685fFn^>K9lwR8L! zW4iUx9ut3xr9Z0Rd#Sg8r9l^EonJF(@nlf;wCUEubSLvkvYAnbIMWw;($Sl%%4lIxU}g zlpu+XFwG(r4U6bij!AKp5?33|_=Z3)CfKT*Q*?QR`PNH=Xu{ub!l@F!}?a+?S)!DGDnRr}? zeDSqG_JaC~j6Av<;&D{?-wpBM4tA-lw4Vo`NJ8Cy#*huJ*H`A}Lm3v)tWUUVx(f^; zw^(Z)Pvdaxc1|L~@fqnZ#<>>l5{?w&mk+wL`Bd2Y|44f4xTgO1eOzGR zMve{XS`h-$A9~2BpyQTKf@X7c9+SEfD zrD~jb>6ctKuMmi{Z^TwicK6*fQk01Pd{v~dW0l*sS!wD5OP804NgeFft|_u%BVge< zo6aZn2_%op0`L%psUxVD@y>vCs#A%vt-UUtNFWJYm{Sm=y?SO>A=9z~qU<DuWx`jc}Q2E3Z?4VT3H=T#+*X@oeUb749brR&LVzYVbVwvU*xN@yz5_^R^MS@u_u z)Uu58{HcI=PwxMn$wHiZ*Sp0XgHlbRV{XrhZA6q$WQ`~C%cN33a9f~#J={r@(Wg${QCl~% z?h;Om$0RtvlnehOL3yPOeeZj+i9!5=+kqezneAO$GIKw47E7thaM|6fFMNu+EWVbU zd%LODZs*rJ>Q_Id2@40WC{TXF(b>8_8q_$wz#?M0^OQ%!GZWEtW|ohdlHr3q?KLFa zHlYlv;9z*}VuZ~>3?KJo94dT?ozpB;ne{2INoE=FioJY16}CyVY(*Oa6v5qhx5FkJ zSBL6_G!!fpo8^&+#d7^}VeQVDyV!w6L$jw2iCpm*YY;H_7%CfBz@cjY_!Tgk@K~l> zEu{FsY)Aq;e=F9??zhxTKc=NcWs>zw_Wr#jshEv^l5lm+*7_hZ%j6q;y06QsSjtP*m{p%ZRe zG!k6R2BslRq-t0(;HlnMh*Ogs#m%p&Op#UDi^s4&3@?7?6AdRR$gypncv{1^E%gtO zVO*bjtM;UX`8u#6P6wnmjeZ+&T#gmk56IRmI_*nLRek$c!q8h=3MON?dNrA`zwbJ@ z)e?Owd1ph~3)ik4?f|IQl)D|OD5B+X8W(fod;0TGa<9#(Z|@Du%PqEh`=`F-QznI! z@xL?5KH+<qon(}1(;Gee<@(;YE5l=i zYd$f?rEQ|SxvoatcYaqc5V2#{F_S^*BdKbS3_|OFaOnL+ym}XFT)RxJqW4&RsKzdz z`ne^Wz`qHvoi~YM-4}RdK6g3$v%QIPQ&euwz8@!>6-DrFcPzMR+Q%V2oODhMclvFE z?nmJ)+xK$i4FGWQE#f&{+93g^&uI%%XUYIdD6MU6AI(UeE#Da&F7cJ^NXFN5KL!ys?(P5)&_i!R zv{8&)tBG`%DC@6%?@!p-B6&0widD6PO4(fCB`#~;{oY$*u-wU2p>3aeLfN9z;;m-G zcdt@F(xqz_FMkt5Eg$WdO*!(=K1Q2pT+<&=L*_#EsuA-8 zCP0!%w=UF4pg8<+Fa%!z>}DnA_0Q&E-+h@kcAo^#^HlxsOf83N=4s6<(Q}=R5(eQ= z6X5S8BGZcHds2@s*gbStg)5p|gXfyEy(kiq3-deqz#)dfGm=QQ#vPs;y35d)TkX9^ z_{Oq#y`!?H-rC#Md-?s!vMzu2gR1{#N*-{77aeD(R`vk{eoi+@M^`#uy;=QYBJ_vT zTVCXmViLl~jaH#bD=$blqo{}#KSM^CqQZQ?^^_6zIxp8N$s0dNN?w)eoxgpJa^ZGS zvZqm#;Z!l%WXFgEaZpFKL66UU9dFg!F8pNJJX8sSc9wj2dSTSt<^xRiv28r!C&lGs zT8k<+O!~aI7>=DQz*zTeFif)EuTjP@_DH051WKm%ekZdvwcYS=6f&TR7bQvKx4a(+ zLzOI*cg;F~k6&2J9Cc8 zOuze6r;Uq#Zyp!Ev;0L9D}EF0^TlD8@A}nC=7N0-+4t?vV1#F!(%x^mEVW=4l3y4 zb^t$|Kk=5z&k8%IBAq7%^0*Oy^;#zdy5n^H4k|+m5Q~2WKFv4Vbb@sjniE>%FUQ(r*Z6Rw69l8%AmPV;Kr9$w-E4<9%{Loqn#R@qS%R zq2F9H?am*UPC2t;qnF*IfMV+d5f{Zhx#D4*5zXTs9>y=l1a}P7OffEVBYB+CMV14^ zVl~_AnS?1ze!?F}bCkRF!v_WB`o(KDlotaVjMO1rL&VG=Y%1Dk>t`Ud ztF76d#(8xEgojp^{^3BfpI4zWK}a3J6vCLBUS(16>a4X_{>eVHFZ7RL(A21)H#_Wu?E^g!a(tcDCqYfP-|wsa4X* z2#Td9N}MHjv$(_J(F38LW#6iepBu{u+~(JD{O1l850Rkz;sP9#8hU@Z8|_{ZypqEN zRVyD1iCbUWXsww}FN4s()xDrcen_U>y|aY=!a<2cs$LJq)Z6BC^2yLEbw=UC7BQgg zFMBMw=5bdUicj?e-y&9|JgI2-h zXNW2BgYnq(GbkX9*4VR`ZE0n-$Pz!F{K^uH5KLW|7WrL3*Z%znc?$P<7@l=UliR)H%7{9C zlI?f5LWjSuvgT`&gqWf5iaO<=$FcAyvRpN!+(RB z3IitTCl2XJv+`ZF>mZ-XMJ(DKQ+sJfoB99!{edS1VablQ&3;$)gH{9TT`mnWY)@6z zkjrL_cCJw+;FTC>Lq{>GB{9~Uutsiu&ghqN)ziLjb0ea@`JwfjWlVA;^4hvAX}fp1 zsfXsM+)&$t>2`Hw0shwQnQ8lPh+w24wIzqU2<>jam}?Kck!~n0;)CtN@M6O>}T13~^vVmplo&iAqytdqPa=Pt14rC%N`EeO3`)aQQ|k)utOe=f!M*cj8yP?m8E> z;E*`g&RyfJLsV5RXwt^CsK|EEe5pn~!(N*<1KXtGC-=YZODaF2#dg_#+VA|B7jQ_s+SZ8{;5~3N%^v}r!YM!;9ZG7;mGc`a#kyV<$bAb{^#?AqFqdD zKNLwF!0}vuQ){A~boPCO?`sW))vGb^9eL%fjmU7vELq_LJs;C(iK3W2iglI!VmoDj zF1FfKn|@)hEACyzUq5yTpuf-39L@JYG{;w%>YJTr=}`DFZiMv#1vilgj=0Yx_~~2muT)SV6rRd5drwDyeN?e)+PWf^QzgEH0 zW60RHoZxl_Y6Cf3yM5oKTKL*d#>C$)zRi$I*8iQsht)LMy7r5nNKO0;Jcu`>><9?d zsbw-siuik})x`}W_UEoVeHdP^H^E z68`(l+Gr1QbJ1^6JmA{nTA0fr)5Sdt&JTbC!JsM^gyA;9-ipWG? zzWVbR7x%JvM8RM~Bw$=XAlnTHRjAA8qpi(Y)VW^s_lb9>v}wBmreOkgJ5 zOONRbt8ejq-38p*P8SHV-G#}#j#5KK&n(ALqkbfezd9PcCMlCi=d4 zZ&0IKuZb`cNEnv&k4%iQ2B2WDXM_X2c=fIAhUwJh?~YdrRZ! z8c|DELvnpqO%?>%+|!W~Fw8AB-MFWo9vS5L67z<4Cg2ABWD~dlmb0mF>hSNVPvEMO zlK(2xZ}Nj!gu=GNZ325Twq2ubur!lNIfH@ z?-ScP(!qdjL~P1oVO|^X)dtltDdIr%4u8pr!zPi2PDa04E**`*|{&=E9K0K zC;SaZH?_AcW&WAF9{lf2eKv144%%!x4(VK61c5)Z2RFaw<;1+|2qP#+iS)^5R!i|8 zVdByk@TrSpqhEf6N)?MW=gu3hG(jK2M-z2kmS#v^N#k(eGHKQ`l2KIO_I}$rFtEqZ zD@^Izt!~YZtlcVHI?f0;{#;!gJ~7Z^_NS4s4B_=5|gLjv=odoYR&TYZrW4C|a%uE55T9dj6FJ*KVM9VRIMw zE}S;E0H$BfxfSiPTcG;KpA4d>Y^m9*Au$CwaiE^+Y0j)FoZGHXb+$~+^0gEHYl9Q( zz-kuq8OEi*{As)7|CCYB|5AjOT5F}gKpF*f9NckRUV~}VJmm+ZX!GOF)Ll+1@Dg#G zirt!2wPtV6vj0>0lU{stEL6Am>*$f7!gW*6bW70FQJG*#oTQl;YR& z=CsCINpQ&C$%CYlDrN6{C97>8#_z)Q(hIuMnLmSCKhkEaD~t`9Rq~&dULwy?5{C*@ zcm&hU`%OsAPiWIWtvVvj=!f~0W?{@DnQQI$%7`RW*6?~)tvRoelmV+}#KL>E>lpDO zQptZ)O+RK>wg(1+hE)$k!T80bdz9@s#FQ7twcP-^9W#>bns^6RywEM^kfHS7VbQuP-zDnXLvHrX{T}g;%+av@{=|yvq$R z{+ZV;WeXT>}Wa!PkdnKyUHzI3gYh~HxrL8DN>j4eULZcaj-!pnzI}d-6OoW6oGpb5 zwOOheK9&FThWh$gLuT@un06B7%dhXIrkG#|o@6r<(wE1yQah`ZJz~oHYz3iGWz_OS zZwqR5wM^{m)*VD(t-ahQ;e`Fk(2S0;N=eofkqxukdqKf&CLHg6xTi&E?rHhqj^SEY z=osqo8kk}}Jc6W{BduCgt^!?NqT}&NlseQIlJkE4&BLhVIXr1(l-LsMfQkRD8XII z)AKAIY2;nbnJ#$mk)OJy2>rsJd-`w}Vm;2SqsFzE27jZ<7ipD-(1EWaj-;KUcGLs* zrA-$d#oj(@6p4bU@IU1$B>dM{MuDb)x2W0-IXXaa5r_GEm>4 z6hRyfpqE~-l)tyBzm2e8j?_Xd`byRP?+h~t`pbBA6pa$Lt28ILodnX3NDGJ9Itfn= zzIKnxWj!h2OAEDXTUzJtI82&Ur`=Z0s%ZFs8HY03u@#`V_i?(mPq;np zyl6^EHJ#p9ZC3i*_eIjYrD)pjgHruEy~Q@Sx3vhEMK%wI*i>Gr{PBU`QFoLo{zDvq zrG2WN-!T7nM1Zx+R`ua6EAU$B^I{pthYh4npfHDqkT3d-~4Sk8Y|Y2f*xRdY$v zqXE$l*K{E9AcFY;zx09?VdoIPE(#tQP@hgJHsxLgdI;5$Ftt6BgJ>|)m<%}g1WfZ4 z=gC_qYg^GC&90c7zbjrkYE+f0zAFbqlGa_10Ld2W%)iQryphqFd9XXgdKKcIp8ilBaOjs7~F#ie4`uS~KSm84xu4!C%8 zGml=`;Fc_u?u#VNK2auC8|K8w_4&5Nu7GoJc<3NBk=KDEJ4%K(bZjNXW)hw@Enjr0 z^FbaD|3!jfhZ41nxze5d&-#0c)B{Eq+#&9M2yg65U97ZcVuF{)9bZgv!Lh6O6v7w>e-?C3nq5GdSZfFCje zM3ZJi|8j19n^I!UjpM=Xn@eq63NdA!pu5E%APgL!7B5?8!wK0EE3-Hvz1I5qpJBIT zjw`r|6lqJ)=K$yIB!(aov~2TsF4y@wY9|&+SP$%~s%(W7R`E}C^AGh9n;6ouHOi1} zVn;7GrB5ur04XWz+pj3*!E4gwi$JxskdP+2kVT8s5zkG?8fjajq(Y58(F<3tI7TXTfKkIH%d>c5R1KdTF+Ev*!e68E>;2%zJXuS{NK#;r|;dFSe?AL zZQ6FW9gf4}HVBPud8X~1{1? zxx)|3Y${<*1Lx+l2GYk(p0LG2mN7cb2|{sDQp{8mITBQ?iCD)b1mKb81#Ep=$fUQ^oZiRsD|M38g=&_FRh!?NL)Sel++=+> zH9AX2p#y8%Z&}tWk(GXAm4Shw(PS~w0hU~ir<8V!%?tjp-t>?57?xVD@l%dIOV$4` zQK}uWFR+ZcU!_LPQx}IwGW%48JN5d?wrV7iC+~0d!2 z?#(WXdFdr=-{a_-BGe<}R$)Z%Je;{I3?i|VsMiQXhwaKLqHLNgqCT1SeD7zwl#nEF z>)rR}of{9{mJdQ6&QiwE)imAUYeA)c4i}(lsM02%OS_7^3YrOwDqrq2F4j8hf<5yK zJ~%V}1px7BhV!j#cTk6rOlty8m+1%@L~hL%g#{J(FhhsSB^1y=Cv-Z*8ow)erC3Va zKNPA_ouT;E#$S~rJC~!@#Ey(0)(E{r9{O=@%FP}OX3(RfZ}+~+Aotv>rUqU}zqO}$ zHDcX$_hGKLYeZ{O(ULV?m0QxMx{WR1IjhuVsc+PBrdfs&@a&dA;mO^guNXPe1sleg zB#YlZNDsLECfl#+oBbR(+y=KV^0dM0kgP~Dpn7KIO9!lTS_(H@JP;q|_;jzVThLfA$U>mr) zOP=Ex|66enHQUa({ToXRop^WK*&n;2f@0XglohF0k-o)OP;tVLZi&L7er+(trpU<0 z8nC>`z~9VUX*8;UQfPsZrztT}ilLWGqrLx8-}uU3nvwc-mA`MpqI15)xLprCea`+G zQhPfEgrcxhqyH zD~}jOZIKp|AXV1gJ$WQ`m_w}ZEzVH8^il*Q_DEfUp&In%;C&)juAn@uo3>Uf{j#O$ znZx-Jbhz(YQQtm#!cc&>^_6p;viWb;^mXC|Cea$*;-794N>v&~xcei#9H$xNhY&DL|(M2kL!&kPZNylK=-86Mr$ zy1iU2$6(bbW{e#}T#-Bbd;=^&!_nSd>-SS(>F#{((}Txlkze3SWR0hF^SG5f z?c`~}sr2zu(}-P5#`LlIJ^9V>CdT#nRAl0T^fb#xtI@QS`;PF=skdrStNbC7wqF;S zBv#*DD37%Fo_G-Q?9Gr6NcZI2&+2J6iwAg%?-#@R@JvCK^mrGK2PjRPw^LMtWR|$z zLs+5j@#ukTr4M*^`SN)S3!m7WH3aC0&pY(tnhPuC>tSuWJKgR4YvBA3F96SP-8i|9 z4&35e>3MsnZgCTB{cy3%Ju#>CzQj)XX7nuL_xG`eG^ra0;$*h{Xxk@f?~&k|8`fHm zRU8^_^4uc&x;}m|)X1*R?H@v9?*Ti&%bsaJE>}~Z`{f(|itjxAzs(DXe?GH@TSjGZ z+haV~jOqH|7D>PT!Fc5;U(}`sya#nP%Bnk%IM;Ff;6jSSwKZW^C*^jWOT9NI-?qr< zT|u8Csf&Vlj)I8HE{OfYe6$8kdC?C!(nael)nu`%abJ~P!FfeY$|<2ho=@I7A(f}R zuAiI|M6+XAJyks^#2rvC-6<-5_o~{HHt9+6oOE-kO7NAFV`%Gb8Z>M;%J?!g6*%Pc z$veGf-v|H0jI@XU-4T2(lV6xFHKF%3XE4=OVPhvZj#GD(T!1z=5IzA_?pV$B@PJWI zPIG%!c;Y_@H0NwmJN@cx@E1|@`GmAXC25F8Z1Mb`8ofbG1xb1_j8?vB(_X^00G`4u zs!Tv>+6VGR1d%uU#p?e^;is#fdvK(QfS>v(7f*AKb;6aGFN+XBX^RCl(>5Qg{I^<= zBU1FvvzwSIdYY!DsYZaC@p=0sodsw_T$JM0x-AJZ=PI)NxivKYYLS*UcZg^bwXWKV zkfoS|j`gU=lY%)0d;mzK2|dN4M$xP$aeX9MqEN$Ur`B8+X~~{;x4b-}dGy+=z~xV5 z|Gye38_1(aW=h>-pLamh2tSH#TYXVIMaHErt<}c1{HlYV(sQ|!ea?X0z-eRen)AW508cUiuh6T z_?IUf20F*H=I1LHJD9@%VU=c3T zi|L`NlFso^lB9!JOleo>zeG5ZXyxOJ)sPoc1+d+PE*Z_oH!UiE`Jmmh#An3G+Q_?q_3-8(?-v0vdN85@ZM?s zCC){wEV%c-Gp`g^R_=^KTBNJ$-%nJDpt?!7z>Y)fXu${QU#!L2e!WNwz9^uOi5$|V z7(GP|hNxw`X+IbdLz`YHE=$|oDqySA3B?(vA(>_Vf0bUg>(HpOMo>IhD}Ik`?u9YV zsSSa=j!gC;Pn_h&MvY?8A_nDM22^!e^}J>=sN0PAJJnjYdAXS;K2>7!y5P3a*jH{A zHjrl{aF&_C?pF|W(hqd1{jkfOn8h@C;fwk!rVB~f46d7x^^iY=*7F~7?UW{5NKXOg zzS!34rD+PsCkBzfZcc!0mXyQTBZg?RPw?$^!8{csmf_xsMez=kJK%e{C+{fKCrR+j z&6r zM0?t)9_)#j-GX34Z4{hQ^XsBLgbbx&>rcB0|L=xVuw~4*2}DiyP-pqg<-3L+_Ym&o zc{jbr3^Mkm9X4&J32ttoQvQ?>*9-yHl|Aw3fxynJ4OL%=NY;jS?145`C$Q)Y?k7Mh zG#xl+Q8&HEu|rkrZpXf$RQGB1r=hwO5G!o%p*sbE81S<$11p=I0_PyIsEqd;?n3U< z_0GwtM^jnj`;bVcRQQ_Jd`gjV($n#?oqH+iMf&IHcRCk2-t=jW!N@<5U=zuQ%o>@& z&EaHC^9cg$f|!EBVA?prCU4WG({U^`-nvTqkn7=$qM>A3-FVRUIUFBo{Y@t2e`i92 zuW_M+rs4YFZVF-CSL_2hw;NUEgtOMNsPWv(#i8phRUE~+meOS{8GHIUK{Fpb?$qK0 zh$f)u3v=Y=7&d>-Aw`&jk~glu5q~HSE+HHVz(%-ByO4mnhVdZiRABS$cMbaAX+ur4 zHR0`ffQo2`X!Iu>l9ZPa^leojd@)`ndjv_r3@d8+Xm=es8$Dv3m*iQf;&};O=>{dM z?#r2F>FEfq5k0nd!P+d_iW!@#-aYLs67P4lIm5-P=NACMqQkjPo1jK>t}(UggqRwi z;G6p*k!3TkxpfuKbi{=_`tf6%M(u&=k^)#$GniWo_woOcsQf>pK)0uiF ziY@b-JAsqb%4+jkn9A?A%tj?|$gS5Sa%7~9!(zEPf}!mDI1T@%TmA*-WmMXO3a?~e zpBmF3@^A-F_M3IobrEWPQ>dacpV{hqy|(^ zNSLrJni%^q%~YRc6GXwv%7wUyess{dosPa%H8|hToo=9FvZ3Lte1FWAr_fI!VCp>~ z3VGSh7xx$LcXW1OUpQbo1^bRJ=BQZ~Z+_8CQL{Ed#sTlYnX*1UIG(BaTvW?%@#tuwWk!3|e zSP-BdD4+)EQjw20Ua%GYCEaI0F0JjdPPXB18oPkW>~kZg)wTt99-M18MBKw~-09@4 zirN=6{fj=$78QCr>R+d_ecro~s+so8yi+MIv0stYS0vbslm%!=4@@)SQTS6_I7K)C zR^Z~#3zpTHu{0D_3W|2}HsIr@2iH!ORX=QcBu=@~$bDVqZQ80P-OSiQL*z8bX`^6S zb?-J5DsJ?GkEJ&eKDe*-dpq2J1C=tUWf#;ZeP`t zVgt!@gX=bkayD;mAa>Tj|7@48W(_{V8~j?I2n@bJRRXssavvio&<;;ot4-P7KB1r- zCUML`wrlkFm@w4w69N#q)}T+~!%U}CNtfhQUR`bhWIMz7o8&akwvHFEYTd8qL;!7F zc?FWe3!4{xE0k<;`H?8%3^0wZsT7Rf0%;-Pn(07M>9pJwW)n%~#O-j5%uqEB$Z%s4 zj}Kq}G9^+V7E7-ngi3w|VY9(w#ZkH$*cXC%0-W#3^~t%e@BmdlKl6S)cD(VvU5j5j z5HTgFl{tS`;Gx3=yTHR|W$$6UeBKB+9EdRI2dIri)Mk+SId07a{RWQ40+vn@)SHscyLXHxNq^F;syV*h$e0b;~LJT$^h)oikm4gNM zHvC{NrRpg+m`69602O*FTmCIx(oKkBiQP~CuBYtSryX^Y^+%!JY?M~Qv>xR$!@~h6 z>&keOU-t<8>zo+UuU~NKJfIQ>LA=We-rFcPrpZvtvWr)^(@Ptv1`iLSk(liJZDHbh zKTx$Cg5^P1IxAh?7npbe!b}M>8m@P0&FHbUQiv+Py6gUw9Yrsj^>kzrlE=L0XTql) zAOz=;DG?Ibzk!{J&)AwuVLB2R{ut7DD3zRJA@)#e3(CS5Q?q+|_PDvSk(p6z{dfyC zf0FxW+q<}GM0Q%1W1V_bKSY0C6M|2At#zeyPnKZ9(Vbh65vHL$)k?RuOVg>a27h0g zVXu%bG~;1Lkn`8TkQ#PcVx@KW7~Nv`Dih7*g z4y#U#k+8OpmDnx=Qtn_waVe*2aeHNT(4EvS&tS4ZQNCOKWwdGc-@~&#e?B z59K5eN%gRuJ0Z!~FI3-N5Zx1N;qxv+#u(0oTI?X_Z?)*DZ2Jm?yJ`4Nz2>xzFkSR) zMyC#jaVZQ$8$8|?wEt^*LK%N$Kx`%M$O9f^0bKDZ>o(u_VV+A<>DcXWTqMC9ASqrl z_9+}oPuDVO=9@o#+cKzMKu5=){p-L((az)ZV%}q25gpUW(veNHbe4@w8$Z{U+%3NZ zFDX&+Yn{3+_yt8fv*9NRy|ccva1EmwLuq3Y3w!zo7udw*K=@cVJ}A*HXC|9Rc+_d$ zx4)WI@CKR96ms_(XyX0zw(uG;D*~z@&(Kyy`?||7P;J15`5!m{Z=M%)@unTpF-^^k zS5?=w*W6zxV{%eU#A&#)?Clk?a_$TE6{erco{4N+cf3`gs|55|HPw*OmW)+COO_cR zDHq#-xcD2OVr_jfvJB0t7jWHWbkOa3>!-;(K?D=HFBS$ioon~SfX(Z{rZdTrreCK~ zts-)9YN_)ltHR3PMlYz09Ld=AX=wbkNrZ}y8)O}ahzy3z)6JLGA20YUM~n!cP0Yc> z$-=pyqRpRFe=oYzwT*_vo}Z)bE_*A==t#wB5&9-GS6Wc2~s-*_}DWI)(8 z*F54#C_+3O^g%D4)qBlQJ6!dF(TxZV1uvedGhP0*zc9pEt3g_=`l>+ z`U!VJyNRedacYiP%A$q;zgfLGsRk&09uMp>Kh!vYpf0x#dP+gN5U_Jjax2otT2&@+7_JxErU3iBix-aoi z$e)2iEFdK1MzwPSVpfv)&*B+ywh*=c?YMx8_MwFiUFDxU;`0me{k>P_ zzf6uA-6=vKH9vz~&ye?vf)dH~*DdZ_e$Fk8c`6rbX_HwimKlHk+TIM<0v?Ux2XE_V zhhG>-CKcN;2c$pxpb67n)6y^-F7uW!;|8`G_zG7^^|zsc16`qs2dwAS;(5e)pO2jV zE47PFElXVl1%5O7zW!8dF||L?igCj*@UV-yhBew4oWm?LNg4e{WUGSPiBh;&qe^k9 z^G4rcsn()g=PFg&yn>lyVx7^t`f-Kq|IV0fB(Dm~YX8b(T9LQoGZNb(x4LunL$ixD zUb(4BI$57OECj-Z?!&lEbd_HUbqVLV3F4_E!oz<@h?(noP|Fx;yJ&EI8&`?0t5-*{ zVvr8q{d~Z|?e@lqaoGYE(yat%ZtNm5oV<7uIrK7U39rlAA$_PKlGolq58IR`=cJ4A zri%^sZbHYda_h{p_!|1ma|JU!PO7oDhX(`R1i3Dzv_D zgPBX?rU;vkqRmCVc$Hn+yVuf4{M|n_N(Z9#`Xu(+sYcjlhe{8NYJ$o|rAasd?8b-q zjN@{dtvTBiE}XYbwcU2-ju_PkbeYc7huZmb*yl+HEnVunUb$=TkVx{Z#X%T$v@SsN zr;FCyRB0S&#KsIv`4%tJ{;>+gqbN_@)lSKZi0bA_@z#sfOOP4k#GY3}0Q=|84t)WfT%|siAqXOzabRjPopO zWFjasGZegY(UeSr$|dH&IqG9bsLtvHX*xMHOE-~+GvlRvZOSlr|AA5Dwg%4-fco3Y zO`%2S6A_IhuWuJNBQFy;CJK9yumtHK8+L7#`}+JyV3C+4Amh{OV%fg}^y3c>`GNB> zD!bF%LNwW5{H0*4g?dFV!k}q5Z-bQ2=b%OrROh>>s?1RdQb%y!VEg64nE%kU3?W)3 z>on+4P1CQ#UjwH*!344~{Iz*7gLc?Tty#`2a{axaVDG9-U7cyR0P@w&i0%XVxM-;n z#dI@*jb|F4|Hc(KMuT~{$fIYz0%EV~!SG?kSgpmITJg336e|=`yeYne?e;P(-_x3k zVX9YEXEuUe^6ttD1_QsDSpX7w?m-iQnEm(%VM<%uW-=7NnP zhOkq_suyb{QPUz=9mGK(ZG~w2RUjy@9>L+esjn=vYK=$T|F2~Qp>62KXtZ;Nv%U)V@MJ&o45{h;QZP~QvXu*K%$ywTzDDDYfT|aIEv}4b3@(mz{gB1Ig^5pJ{UMnn>5hyoMs~I$1 zza>a}Dgf0}Rc`+FAPxdE#t!Eeii)$g1etTcUA(oY4Jm~#NPoRigMTSrBo`2KC)7YS zBHAyGIPb=js}$OHTK&jVZHtBiPgLb!|`@aB0)0#}6HCJHudm=X2!E~JtBMCadIbfG#I;*cMTs8iYWiYMeHj&7H9o~daa z?8|2A950W04G(AhY>#-E9~S&L`s#J>&6HnUEa4YgBxw0IY%?MM{3?+} zH_|KYrYzV2%c64DziNS^e%qMrbP(w~1xXbDF7ach$Jbm#RMJn7iL(*im}}*Rq9J71 z(nf*IvW(R&tj+66#EyU7hJBEdWZrNatwMh_{2O-KAe4Jg=T15L@>!s@_&cuVN3kCFgS76t1SZKNq|@p zZL>|ueueuBV0WR<81p95H^kz3+kxP9dRmZJ|Jf3151#ZDQ@`k%SlxMjzC=kQZ+?HB zW3gpH;N7oEpS#f{J2p_jX{^(F0m8$Ur67vHa8!MQZWhU@frIf%UTHO)?`{i7u;)i_ zpxvVhulVCRtkJVinRIi+KR<%Ewz-Hq|Gc4`+GL*DlE@3EB4Ppo>@7Op5RSd6LgO10t5L4@|YB0It>?fU6Q= z5I`vDYht^sUa+I@9kq?-hR%E8T4iIfzjdJ>wB(oXOegS|s;IfyadNdp1hq^mb9kRypFhrS;v8m6#dQ4{an-{a-rFW}<7pXa)+^ZcBtPv`zD?LiX`Xo4IP(B|v|xAw2bv|D3M zWTm&YV$W1X6*UHAAmbA~e9ozs&5aUwupKD!a_a&9`6>-@Jo!#(8hRpz_Ryz}so2`-`~^<@cG3)qPdpArs6)zga$PV5yEJOcv6`f9WaE07x*!CPqY0np6EX zTo7`UI#sx!yQ^`nq(3aStb<8As@mH0%BAymn@W#qI)aC))UAbwD#x?eHUi~*f9sUS zx|-?RlJ($B+Io=>%qv{gn!q8n=f6pQmFX?0fhw6A!&t9n`J(vb;M3Tn^jMNHt&@IP zv>J^B#UtF8D_o?_=2{l1|2FK|~x4l)Gsc90_1ZFw!?V|Th8~jAO$&Bl0efP1oG=ERCvk&)B6?e0oJ^4#=U) zra20S-jKU)fD`XC#keS_`?zo((M>s_I>H~Cf@HPHojifqSGI)=fTR( zQFWMs!O-q){xBHKVQD^Z1C<~thbl-) z#H63$Gw>=Az<#;YJg1s|wzP?wiC^WENjBzypBDK_P6aZHRnwZ&*@>Txd8>Bw zrBI1<3z-}?Z zGRbtB^i|nMSy)6et`J0b{l%Iw#fNJ8pqvlTm;Wn=@FvJ-L`0Pkq)X)Mh420s6KeLq zQ{g3*&g09G>Q>iDD&8-lS)SxXq~noS>K%_n4U(y_`s(q_4mlGR5^|(@-vQ9N*vJhF zS~v?gz@(sg9BabH0kzxyEK`1;|KA;lQ6)brN7hQ8Aq!wOVWFT)xxD35q?Kk$U>irl9cCNJ)i|MBD(4n&NIjjX?~7^w4InAA zAzEfZ?)&8GF{YZt=FGTKc8ftwo5;m^d*3~Goe4dRH7dxy*i&S>hIDz)rKmK9YW`(w z_Nr|b?900(={xJ(N~`)AVc-)ya+CLx50lIs*Z*wjNL>OCM#0)Y_4qw6K52f9wn?qD z1$<)$)1^7Y*r}55HI9_@$zzTUyG#C&z}!w_au=C_6RmWX9TA-3CKBH*12~b6ER_BK zgx3ERU_q@Z)wodI(9fOVR+Mp>RcPT(RF&EHU7zUshky21!1TVIW7pE5t%75V|9YRP z82PvrEGk#6$c*D|5MsG!k8Jn=@qBB4%4yn^2-!SC{W+Ll+Sfz5plh~k$vhY@uJzX0 zsLG4|g7PPo0e*G3-sjI4BMGPB7wlOLxGr>9ma#JPv%Zga`p?FpPVwcwOFUnqIwMsx zJeYtKa#Jacp5{~r2Q0&kC z`zn_COcmQ3fMnr*@h6Ob~!+VS@CjSl-7UcqB%X@1p+3L=%m#s##x81Hn_a zT83w=Z^frcw_FZv^pDr=Vm@*M$l9py2*(O~-6NMk@efpEAP3mD+&bHvZxo&_p4|wF z>6Hm`#+?hE?K-{rGRxsQe_7dNP{xiPRd8bF*qcg=Zvq)nnQF zQ6VpfJ5X4w?js=a(5Tv==Eq96qJY{T5oi=Gym>T-*qk43dYE5^!Y}wUa9O^}*`tQ( z9pFp0K6B;Va#H*cWXb7)HKiMVURW>W`g&-sHc=8?Ka}IEk$BlLR~yTjqInZA`eN zF+%fnep{M^3~CZL(1P{$$w5yy2Nq&Fc^fPrKQ6h;f{AVzD-q14ZQ)II@^T_S_&`3) z1$Z2Ud_VctEN;xlGHeHWLphvMA@D4|DPnLtn7AILQ2kbSX>9(cMnT$~*X)OMJ_^ju zG6HO*I&y6sDgq7fO$3Zj`aT2!Loyzy_u!YqUs-6^Ck@U2vzNJdht;9hz5fnCDmv%~H2vRoqL`!xT6v6guM*b- zlKLv~fhIk>%#OQ`1k~;8F5aJQ-}hZtt6Vcwo|!`vRQR0@#FFH!Pi6v~8kwWpVYc9W z(?i}lOs49|8uX&D#hjSj9B?A$?i7aqj-*$^VQVh~ObdIPw=pxeEKX@FJUM*5%w`mvOy5vX2xi#6;GWHb&WRy{81-zAD+ zrtH{?T;dLBB~1H-g?25GPY zOc3CfI}pw5pix8*QzxE-pG*ZBkLpe4gZ|RgEz@&OP-KfF)()DOmLG1aHV&hFIaAo6 z`Y@F=+ShV^!QC=YU~n5h8ud;@OFex~rKJygb;#^$fT!a*VvtP;%kHRITFM6>+t--< z=n}y0MUzg2>%W3Pz^l7i^_t0)E@0<>$Fd9{6BP!WSV)fHRYUBnvZ;Fx1sOBF8xIlf z>*?c8E2^9Y$~^oB%<5;zTCu7x3F(f(0=JulEDOhM58*hsMFsQ)9h{ltI=_#8^goJ0 zJKii+rqgSg+#;MmXy z*P9>kq|7jUo{ndl$}VypqzB!9R+vr2?Ieo9Zsr0a`(l!|4OSF&{w_G4n;~~*Zde}%L&=;9yZ5<2v`=w0~$`~$`;^gX>Cm~ zaeRhVm_M<-2Y;+~KoAH9X>84an6Gjjw+>i)ji1`lwkZkjF}c_iojb}Tdm>^)XmU{o zASh&Tu`!5ko$G42OnIlotU~i9KY>C~kiNJ+EV1kT3JK+@SL_XdeucKC08&iaZ+4BI z@a>J+p=u5qdPYm~l3=-9~Yg;kE z%yALDk9ij-y|;W%&E*ef$%wdnDlsu1Q3>cLT}&#&KQtZo8}G!6kb0z`hrCgKiiTOb zufREf@0Ay4eH?Mbd2{|sz_Ss}QR;FE*k8zSMy^S|Lhx60^}Uz^>w5G05dgofuRM_UJPrst(bx|4~kAX*PDjYT2l;e%;BSFNErA zmLfj6Z4tktu|OPR_IaT2M}*G8*M8~b$&k;G@9NQF>_9ba}edKEpv=s#OF%+W$@4{Q`UFVO*fXMT2j+|VP{CVt_%S*M)Sl?V@U8S1p z=DU(Lm!FVMp;qfD6D_-c7L24E{Z>)0P>oIfSuw8Yo*56j;D_ zwb-q&GW9ZVVD|AHRzsevQmCqTq(#%t7WqYj0Kd5l+W0a&FbjhBfLdK#-2MAi-Bp7Y zZyT>#zB7*rc4VmJ=MBSdPB@GHm>akJXaN^y?>|EGmqbtd(vH`Bhd3*s#n$wH)-V_{ zY2TfKfWMe6KN&hQ&i1v#(E)M&K}D0(3(@1XkebT}eg5}S!{j+!Q?ZH{LmrmD4jkV< z9z+Q5a(8G}WgBP)>QDmc@6R3@A zUp-r#`9uDag_0DnOUY22eXx?c^)Zd*sAqiiN`mDp=oua*kQNrPf(p4s3s%0krWRk+ zSfWFWSif32V;Dzt>6M(x@?WmS37xE$LwKFSNCSAJp=+Yp;En2Y!5ES=jwIJ4IU5P! z3+v;7hd8hZ;h(mzzQOddVI$U+cQ@R@*WoPNf+|JEB`F`IKX}j_yJ;JN6saz)JX-Ny zt2zTPpny_}3$m;UYRh(F$ifY#*%`;IG9z(B&fQ6G#@+Q`Z@~C8?#X0gUXj4J*}38n zbp3x%U`0FYT-%Xg6|l5HVFq2uX~Jdjd3q1wEqaqhB4vM~vp+hR2?{ull11+5FS%U8 zgRf7eYR%vO02p`-p|QuV`=kfX81t3nn|>F{?78+qKHI)(*8lgzg;s$RFt0@QDWvbj z59aN>9*YgyvFH&4q)LzeJ;ecS3}8^4_yU(8RoYaVRzO6W3*vB@sl4{7R{qZgG6buh zgPzUz9b6QbAzcwuG06z>@QO?MMc5y!MZ>nR@5P5-t6nGg^h^nuLa6RiCg&zF@K%_4 zT3v>Bynm)69al%a6&2>1v_h5^i5-sln!%WB3bK73DsF|p<%)KdJ&oaz+}8csRY`yI?nU=g`mIA8|E_`=en zY%c_ss}weao;!YDC)e${F4Bs<)`y;repT3_qzt>jIoXs3W;^cZK3!$v+pipkJ?({2 zOp|zK+vH-YLhX_yDX6M1T+iR{@HIp+;PH|^5tN7<2Hl1mI_z7N=)~MMQ(oKpev?{# zF(8GshdiFDrP`%+!W%TCT>0d zgMzhxQrnUvENpH0@EWR~n>NbU15=F-yx z-?>4nXM84p89JyGaA%kR!Oy^$7pl9&{$`Tzh3oMPU(mP3m%iFEN{(-YyaP?_7PnOe z+Y^$^Yeu5>F|Sojop$zmu-cE|NETTEj33kVb3L`yZwdwGxkOT=4$t@50s7=?VcG0! zdu20~<_-c&cHWgYL6`C|MM0BL|g+%2gZ5VyT=+z|RFIzI`ZmD!9R zHk-!iC8?;qmBe+tY%11Rm|mzNdK-)wO!B|-oE0BzXd&WAC6f&W(s1bX#a2ur9sa84<%T0`&h%iNdUv#pe%Se!E5K6y0i=GG?t_hB>+r zM}BPp42B#)*H$O{NXglzy~+DrxZgP6>h#!8iatHgw)Bgs=p3;nr_Zv=ecV1swvadB zYJ>NDU6Z;*KJv?RKf_*Wfki_uSFGFmRp)_~{fEvGtAdy~E~Kor)9be5cI=pBqA!|3 zPM0U>3LZW+R_8;_xC^-MM51`@(n-AK{O*ii3(0oow)MW6ZBnY$uu=WbDE51A{@=Y~O}%FznY47ireW zlvnSc==)ytQJk>Pr?&A;Jw9Y{@I55@=j~PrtD@acWvI$ZV4mGaP>AsWT7}sJ57;^2 zH4aql0d-RTCE7lR0+Stfj&%@vr(u6P$BIuk`ZusF<6-g+IMsW-z~OiXWt_#@msd+X z9m2cclC+{$RSY#t4jz1YEX|%Nf`LYVZfYo-t`l#&B!@Bz;zMy&Ww1OAH}fdPvFLF< zyAUSO`E#yA{!=p0aA4Z@C@{*!HjUdXx^74ERmrQRN{&L8ycsU$n=X~iAV=HRt@~1H zn@@z_0F1Z{@4|V zfO_bRn~;%StrOBn*!4>B16q|r@e3^)nA80N0N})Ked@l5< zBBySQOddowHQ;{{#8aE0$->w9^q&BdL>3)}?AV+5cWasH<##nT<$55OxeLH^ijxJL7|S*t5=ci&@pB3ds-(CKj zt|8qPbibgH3Q(U9qNp~*W;(O0ct+QI2j^lDai*ARuForNx^|H64exiW!Q)NK5{aI( zUmeNM7JOW#52!^(X#DwEkbp{>usXqEsSi&egyIQB$?Q4FfyS9G;bVL>aN4(@@Sj+%=sTj3PD% ztLy=YfIq=XFJzhF%$^*1#XI4|X$HFGO09RHkV~GQEp&`L@-^1Mk)T zx$FB6Teocgyr%bTx5@0j*4;{>)O8<=H!$l5De#1a4d0+AXTL!wRST7uz1H@DWzLzH zI%_p?HtnB!J^bRUN(poE-G8GBvqb|%nVg*!lg|r&2bG7v&o}wqsUxcEdt(C0Es6Lb zDG*tsM9E)VlQU%7f<2GwRNWO;#lRs3;5KUaDM`U8x8Czd%|zeIHnpf)0rA63(W3N( z>R4t4(YTy#?}|n{u2`HwY4R#t8yqx)q=AiM`k=pzz`W}e?->5To^&;fhCSkG{5u@& z`8$udjYT41w+Ggz2Vaz3Vosd~KJU-d?~4uQ>%t~f#8#4gL^-B^#jhNOJ(uYAfH=}Si3VyWzFErQgy{jx)?UuL1^8ykqU_0QEaQ~bs$pxFM zqhI^?)oZ3l66a21_ZxS$fStp4VJ;=M>V;zK6bx4Nr{A?&6U;gN2_{-Tt!ys8*?W`M z{#dMsE9~z3&Pgr1Vi>D+yqnT+iZ z>BRsxpvSQL#j;*c!7AyVvXTNax<2$<(uX}_BYL^7e)%XI`m}uf3rbqk-O=5+J9Rf+ zJ&%*hVlnrXFV&#%cX2D+b3mS4z=_rKsy2tq)L>uB-bV(Zp~NKs*` z`kYo9-k=`Z98CSpghz}QONAIjz%A;P=}gAnX!K%!Hx)+eIj%(sn)1=-2e~@xb=IC# ztim9lT7{CJsJyaba+<*8F)!)i?(VuY`~b_Y((YqyRpEUg@BNm$-=tZk z=T#2z7iLobj;5E?O~b-L_K*)96%H zcA>O&5M{Dn2MHO{K-@4a?aX%F72kpwKgosRlue`DiG%8dkv;c#ffcoZ3Cd=Ai#VU( z3KE_TZx#h<1*T87b`@1<|P;ih{QR*BD! zA^};-d;0We=h6DR^phiZ%KmJVtPG+RxE%o(aP9al z6eDr_+XN)&B}s%gz6`T}!(h~ot?jnU8ik4Gi*%cw4GpHiqL_-s?w|C`q$EM0TIk8Pw3Sb270eeD zROw79qeH^i*fy=eKT(Vo6BX*^Jx#^a%PqJA*&7xLiJ}s%#g-0*@*{&?fEkQUU$7(2h$|4x zax9FhTycriI&@E4%~hU=k^5};^YkHg;!#etk#fRAxp>Hv!)f*7ZSwY(z_am*B!dXr zx^daIYYyU$(FkQWPp}B+pIp2OX)6^PYG8}+gjTuGGF{bE`QS{C(4bK3(0Y9v;)h9I z-4_I-kW80#W)O=9Jf{&w>k&wLUx-$TdsU#iIfw_2#8hb=@4d*~QOa>R)*>cp-hpfMgfy}V zOK-tEv#L$^P<^V7>~!qpk2Kx>x;uui-R7OYObxd`5bAJ4ezlvg!0Wa<|L3%zmZi9~ z$^5#bn|LHI){E~!^VwfD2oXOiUovaAUQbjx#Vc*KQu6496jeaGCz_rc4i}TJld9!; z8AKr~6&(U`k~;Z{G)ILK3InXJw*1bOmy z29K{qg#Jz_RU6VFxzr}H_@bmIfQ8K3-}ePGlgP| z`piZ-WKJ{OtPjimXzaxto4Z<@q!AlLSjLYwdBEi+rjLga7koZWrN`UbQ`46A2^&M4 z5P?8AB1g2fpPRA2G77QZ0Z zR#RK?g0+go^NDD3;Q~?efiFVq7@ec-m87n7-Pg+3k?Qk5Tq$vUQa$l}X${;S-_YOg z5O|6D^sBBn&u6m%R6_oH1VF#<2Q=8O^sDvQJA$4oy;v)Z9z1Z2;B5Kn3~(mIIo;VN z%kPnrHmIHogB=k2{?MO&j<{c|baX~X>Jhghy5jR*r4@)jeU0@+*Y=mH#Dq1-`eGar zsc^sTvK2!uEct16DA0eJ2xw~-Qe&^xRaW>7k$}DVmm{oT1Mw&zN{D((!HRXXN z9IJ+vtDO;~a@6kB?u^2T^&jcS>1LgUIdP8MfE)iVJ>I-^%f;=>Y zz%5cUQpt4AK)Rl0m#fB6d)MHdqbQ1da*pc^^bY8%ZUkwbl)ey#}FT=8E0MclB zRHwylO-HFehW&@a3YKIH!?oy!DNYn2G;#bY3_LKYnW z)jQc=hMg)Q#Fp>F?$}&0HVxf!zHM;J*kS8LeOAN~N$?1c8qR-{1NU1nNO4$5Ji&pF zC9hwq%DT(>4DW$Op+mOFZEafW_$*3}bj@AT6g|1wZ;g??im^ z*RBuo)0A2hwJX_O`VK+gYV%7ZE*DOz?JY`{34HR78oq-xA7;<9sm@|yA6mSfA{*nd#0vc^grg?Bg%wdGX#$+0V4;RHERQ|sY_+| z0E`Rg+z^m$8)%#Ct*ns39Nu!wPxxkZ+fazjN~dk}3Y)iWbV`WVs)mRN7V)!U1wtnO z$9ld7N->X-y;?ARHC(^G9uD7fZM(hc9_V44g)jCnX6F+n2SqQLLg{1KXG7Upu%i*E zi8SqVR3mNt!88A5|GMHsjFimhXJHrDNpmBy>OsDDkYt2;+YGLuyqud0{sQRNoAh38 ztxtbyf-&1O_{HmS9AHPUSr86ZJ}h)tO)^$jWt>piB9zd)F6mqG`$NqsAJw{%3Et=| ztqkIg1n)F)vPfskSF>s>>3ZSq0tcfqGh4reM1pfOh!BjP6%*X>j+rj-vYD%0$Sk-Z zX3R~+mPO0VV$%aGxK-Bne67)|0V?@``8TSRje~$UfLv5!X9B&5Hl*T`wwcs1g-Wcg znK66UIFgoipE*n8Hg8#R)w8l<%U;Urk_1=Gq8zJ6C{#wu;nvN6OB>X4*_M=P9&f%} zXgV3zIO!$-zaJhbR>x(g%o|?+S_pj&aD@5pr-3ka*DIc2H4)ULV7Dmo0Qjlij&u0mI8l9{}6K4QXj;g&<9y+FSm zpFscUChgg~oyTKNL_w}A>5-h}7VLgQ^c8f_X`@ZkSc>{GSI?U@zwM1(NvB*zh_Ul* zpAJB#;T0OS5~;+$l^?tdm3^@zH&)N0RBWG_vO+or^1Sz})KI7LAM#qm*aC)dG#qg2WTJWnUlv+qQTi8hO-=EK z|2;)avd)h~>`aEx$Xz^TYZAE1`2t0F=KH#MjYr1%5s#2`YVb?7 z`z!jLzs%6{{54NI7&d@fJG#AfUhXc7K7{+K@#OA*4}-2%EafRrx}BC7?TJLg-U|U= zGHOrJ{y31INRQsbw2#tzUuP&vGF0tv;{mCzs`}PPP+a6-tnKZk|RSQ2x6qOJu1sW||)}*rs((@XLJ!^?NegZ_Fxku5)+p3Ni&R`%wr5?_f_g|%KrUMNJ9hT%^vO8 zV}sRNw_J3{ye`DSc|v5bi|Yv7+6?ie`sGNsG%}3QX#)Nfz-iSwd>~M|P$}Ekqk#jT z8_&(e>5vVR`ZKaq=Ip+|VfUS?*>IGs5EcqFk@kBlkGo72{qU?3eQ-$cbmmPM)_wuT z(23KWOnmE~TDJPKwJ-6U7>L)oLTcAc*3oDWs#MIS@cRQ!BaaP7ecKg5Av{-yr7573 zHGLmWs#MLDl3rCwAc&h;iJ>%dwnQFSe830thordc5-aH&OB>B~WRCHL@dHYyY0Y;` z=2hP!wG&KZm1$zsOJ@61s3Dl-xXeh35Bmk%@KxBfoGeHaQzVNDf*6)IP*t&JEVrxgA9WcBQ`ce4vdB- z`}0r%uTVi0^m6DlA11KwBdo|;osjh><%d}Zs&~$yysx;#l%_ZX)JQkFRu{X+pKIaFzR^Vc%K$Dvsa=ZugzP-4KGgFM43@jn6V!}U~~6jpqd)$ zhw5X@M!U|D6>}dmar>4Z3q>N6>Fn`G`QaEN^%%ht8%Jyo;N$DUtX0-A5;=Lm-~{j{ zJ*Loh+P#t`lsFUQ59TpS7{<9EfQ#QB!&lu+h&FGyr8OtFPoV&&=`=Z_s`{R2+7t3K zFrylYT|J;Pe#?W8Z=;@nsp9A#oG`jWgYa$r^n(8I#jl4E>ZGiN`MSa7;_jxCbu~>7 z^NzP4AnN4q5o@!;;+yGwR%`TX0w_mrU7zg|l1OO9^lhMNdeMF*H^oXy&)+e;f9~=| zEo?*@+zZHhGaeguQS`}tihgV`OMf^-oMksUfQPZ4na;dwr^t6y2|hy|D;weTekbJ zgUo%2OP}(q!>VBEJ@}!0yQ^UTn`PQSWKRMP)hhuVU@6G{7xk-Iw;ThtDu)<;2|BdK zaXXg>ZqSLcDC-OQ`EQ9MB?S%|xh~4xrN@or`m9P(G8Z z%fk;Ff+j5kQscg2>#&(g{`o1J*hOZMG)3n9ArFeJXdJUkCy_RaXs4bG*>>jBDqH`( zV!{QcK`~~V_M>z^6_rveG~JXtB{owAMwB?iJ~U7=dSrGP+^C$|!z9Y+bGB?811z67pj%wo1@ttl4f1 z-p5Ci@*G;*#!86RiPS~&dfc)tmDQWWx&#@AZbNAwrNz})ue0x4CdWIqu^k-jXkl08 zgo3bh7-bRUD_#^VS<>mwJb&?(81cbOw=7AaYLvRGbqe?T^ZR*-3Zwb;IfxJ|r*>_OB<2Vv06$`QmfAxOHbqk&WeVOrigwd5NvDgswZ!mW znEx#?aIU~x>+m-A>a#bFu#hjBfhD5klVfqjz)OGOolcwS7Z{)?5-bpjUgJ3XdokF$ zBZHSERjf!#9m+FswqV}znokZjQ`Cj*vnmH-Ziwx6o%q!V#S)MkE5E`VevdYVIs}$F zBT4>xqE%|_3*X8UXQ927C~T=M8{HRUG5oh5>KtLlP$#PUg^&JleZCFcmdaq*JLSx4 zkS@))vvoS^gRzK?FZ_plyzOIMMg{is5;8EqgOEc1wia zHekmj^<4uWOJSEZTWDxeFmK|U^3OQ)oD1iCAKDCnP?j`$G$clB=y=)uw!u*G{4v5P z*1U;=*6*w$^|4M8_o7=zW#TZ)X`4a~RN4Y?l+<0n<;C7Xo%-HzxGbgBDX3sQ)qXn!F&zU~JWf1G~Z;i#!)FiI`298|9CmDebYc?g6RC-coiP)BzsRi_|i>JYm(;bp!?Z@V*YA>2t2brk$yDn0%JuKVG0 zN1IKX&J&WIIztb>4tC|H-USpO2J5v}%poJ%1lV+6wDxjtF|4QGaHbyfG-xJ%8PvQ_ z4*!r_mSta`0#}Hr1YzFGE~YB_CXz}^6nV@@a(f>u3TxEvT*^7)z<7BSY03S~%+lj1(y{<}p9E*?VEBqmR$ zD>zxohxP}{18Uz5F)vMW6gpasx^murhqm`un-oR!2At!2(*r93EKz3Z4I@XtPm`*R z`a>#13Ct)t!JL?Ml4yA87Sb}9iGQn+>W=X3)k;dYu`n@Ht-Zu7NlTg>auOaY0rXpL z6zmw3g2!$u9mYHCxOhrr!e=dI)vwkn4|UgA{Fqqn#5lLhD+N=YbHJC^Y#s!Oa?3+f zUQbRS)-yVFb`bH$3j8T|F2GiU^d#4hDGT3ugTy21te`ocfgIQz$VJ>8~+NsJ|()mn;N}^p?MB!FUOaBMP?V>T0J@*Ahv`& z7qaw|ar_DJkj6=)!Z1IzYtT)F>P|Jkg8nQui>Wm(MoK;rH08;M%D!Z|>z-r7pj_(& z9=qlKZ;|G90UgBO?PX(H0euZKK-RtFMf>YtasE>Tt}<-fO^3}63(yQHi6CfA7WLvI zc#rrc{Vu^P0a-7Deo^}GEdo3$^j|0N6JNP+N3s-UI&N_Pup6WRj-J7^+7czr+4R^Y zbF*gCo@uS0ylcAaqfYCdh*QroE~zS=InWg9H8wM!bnS~2KD;^!%J3a*{w1L+1yuAR z-tQe~i}Q;zRdww;a2;%5YOMyW0yS_ndxyF&Ahr65o6ZG-{x0(pwqIsZ5mg^Bd*dW*Ima~9m|LkrPp|zgQ;RTr z>ug?aZ4GTT)>n|cjA1-GpkDb85*K$0txYeR^|!%jiW z4(Gc&AT))5f58jtdTqn9xqh6ZEGIEu0z^PICnw)Z7ZhyqbY%bIXEwt(9x@^&(>9%H zcO`gZWZTI%=%3F;Ha4@!APEMmK7jqF#)kEReOJ}x(G>_o+MklnWRZ{ zNI+kux$%Chj1VF}%)2ew;>3=o+ik09eqdXvJW3Oo?#;{W-n;1P3&L(N1V@rW=Ogk* zUyK9S_agXdMLqeDZ!KcjGlQnGL$N!`1@P~n!M6+SPYY*U?r~{A@rQ%B^w-Uvd+ud^< z1cvnZnfz;J8H*(*aak7YLDN^&zZ~Erjuu@DuKZlUUB40Y%s)oJJa(&%d1grMD5u_y-=NI+WJ0~D z35-$wfPinshU`lWc{|zCAe$Y78lV4o;6@ zSuazv7UUyL`YMog7v)i@f!K|6z}XZUHp8`S4%54hM*Osi5B;(D$YQ zo>G9-Rd*rA>;_dv;4p{uK<5#}>!FgaA~f|lJZVGg;pWJ>x6P5aW0Bn6TlY~%Iqjg$ z@zIkh*I#t)XDp{t#C0cVBuKGF0MF|QKWXV{f{=w47SX~q>Yf3%Bj`<+9j!*v+AL18y&S?1adKLBK!^O(|gtOqu24SwT$)D3*`@$ z&6oX*xubKw4*N`A37W4_uu)*4H0UJI_-gOCp1Kv`FGkg09MBv&f4frOr_(p?3!`MO~v;>$_Uhb*yRHK&19wzN@9KH7)26n|O%2 z5S0-;m~>n3FtyNIm1rY!q%E~|3x!Zn11qNd?o<%Jx|EL`&V`lIv&$lf>lEU`)l~Q` z8#J`UG_1rQUrfDtqP29v=XD&&-AZ&-|9m2)-dT2SNXL$vVb-L7!^cb((EwaYHF=@; zWFsp~^AlCHA8*M62FBS4uR}qE7*n(_V<9rNVvZKN9r;F0-+m ziq(qYUGO)6vBK-&U$l@4qlinz5B2pU8ighliUX~UF!(H8Ioq^t350@zHxDo(&Qx-U zLp`CyPX?J7CsYd&AKHTsTen!CwQoh@1yKe?m*IS|GM;PkA@o7m+z(5hp#D~WQwHtS zJ+*@>{RzXJ+^3lz0??AMOicgM#a8ozF?7@HgwiqmV7hy2;T=zStYZ314sifESo$gZ z>8!j7P*;9{x}nrA_lvyje@_JrLj7JWsxYIQ@Ap<-PAz_u?jX`Y<6MH~Lw+t4LH{-4 zK5(_}u;KLuZU_1QR7G8oOIKimuLuTkHaL4-ch=24x#a@~58lLo7=M7Gh#JpSQYW-OG6!9~wM;3B5^7yhs zaG2QE{}Bv>xeWW^g8bjS%A$T9aD*heaII#c7- z70G^A7I{`!ZD&xZiR(LOzoA#vhnQ{=ykSQEB zcIeH48r7jQk8((k61lydmO?+5H5I zw5DcR(hSY8A3t3CY($_A=3JbC5eI+|x-Dq9LovgR;B$=kPWwJgiYA;cbzxMzTV4Ty|C)ciOVLE*C~|HItjq_QyvsLsP^9 z(&r!`4b73GjZ`n6dZ(Rb=aTMt07#m2g%j#^?5#ng*x6Z)u&8J7pcBf%4pn$_NK6Zb z)_MvZQ*_>9aQTF|O)JZtpU%2j6^O^YN=H#7&&o~?3ORRz&@H?4w7#pp9Z|Vr+$MPu zYTVSTV}9Tnc+&J(ZK0$SU0B*FhNPQNF6U`swkwv~|7*ieqGl3dYl8FJg=#d%7jeD%m??RhRWTQuT0+!>Tm?3 z=KWIMWk&;h6j^PF=)cOZ`cx^u5a@0>2TZ!D=n}j)VzWll>BkeHQ zQ}p9&!f_1BuW-K#&J3H46(<$#QeUq+HY^tG#K>}F_k1>p&TIi>#vh6u=c8fIw+l$) z%ve>_6rW2jR|ESr9{L7*;naRM|C-7cV8_NviyTY@gELOC{I;z1PJ3g?1^RgB(fNkY zy6!IE7UlxHS`DSX*QN3l^lN}9cA`s|z21~BwTlF87f6%V6WhhB61D;g<)Zy_YaF0e zWb>AiOvYz-Rr)QLQDbZQg&19v$u~g(51-Giav&F2dtGUrmS%AZCNz zj1GKuT+|f<>6Ez6V9pjYXn!anjLHZ_l$%)(k@5()e2c(?O6w_3Rqoc1@4`1Hc*m>E zatKM?erY9xQ(!7o3hW$%+p_!Ua7d@~UWn-PiA$DItI6;(n_e6IRpL9x0& zD-l!earxD>9iLpIC%}*w;6KgzG5P8xkgtF?ulEHp>gt=*k^hgVw~VTz>6$s)>)zWxX3dXTt7i7}bnn?+T_w1Zn-7JA z;YMC(Qff=18WLYj@U#rEn+Tl{4~*ePrE`XYX-%0z5$lQE5G(Ghl1UY-u}J6O-$?RD ziYbWi?eQR^SWi`K3`1=9GVzy0kdaQFB9?e$ydJMeNx&Qdo2c)-)kM8V!)yvD4Rmu#32%~V_}ql$wNNs&g(*{|AyILKZ_qk$pA;w06u9kvKP^Xj15T?`3EnC zkqL@1wkpO-AB=1Q!L@hogxIiXPUXeib=*&lfx)4@Bvb+hKoc&yQm&Tu8ZS%99f0*i zydIyd=3APXm$;az}Pw6<{MPlI<6}HC3b6nrf#S zk!Kp=fkz)-lgKgZW7!}mnvW-eYDA}98JGPk&C9{5f=f&F9!|%M&a(AqV?R=g7wNk~ zWTE>ftJplnPUY{E2%=KD(7aHJbL?bF7T|6x#IU%`P2^aPtm_n4afe1+WEgG-R3D#BnZ|}RN}A{_3LTCnrYABx8suhKg7q|q=||8T8UyQ} z<-|iVDiE(goN#Y0l}%O+r@vyTlZ)?VLmEo2eq4K4JwWWo`FH?FCG;h|6h^>^Q)ElU z>kKXs8)ZWgi9SMzYHqQ>z$MD{Lzi1{m8yD0N(zkuz62WFOU^t{YSn@t2{=a-BO%KP zre|Ci$}cC96VkC?getSUI+lE1$7|E6p_<~vwUKJtiYMy`htR?_8?KAcfQGg~uQImI z(-7TW(m*&r4y)S)DtUmF!Q(an67MCEQ(P;~rVYaqBtH^EnXSjTX-Yvzoy?vcjG%Ke z!j+&^hwz(8OW3!8-zR9iD$aCKf5;0qo?MX5_4Tx4`PpX}gcG~{M-7t8`=l81k3)~b`r z>f!F8X%@8_V-zE;W8zA4<$(C?3s4|x5g7Bv;2$m)gZkaR{wKMd;g{BJMkHq`5iB}- z^qkG?QW89r5{CfWAzGYeIa*s6UWk_dtF? zEEc<`u;@?c?Ch@{2}-zM3JAfSRImy=j&k)J>8wgHa=4MFG}#TfQLv+Fpy)aYokLk50ti1ydNw1s zFIjF2;heFLow1(30PQ<1Y$M(1xef(K?E9EpWZv{pQa^^x2o*P_PNo~^347`|6lsHt zP0xqnuTGgjij-*X_ZL`YJgV@$VmN2FO+^eBH`cw;C@+C)i5__eu3g3$1zCg88Iv?M zS*1L=b~3kr1T94_(e}7SDPotCKd7>=F_s1~L2EL8vKnPSRrq#!bq4m<_H;W{y16<)HPP+Rf&hj3jMZ0K&>EC=%d1{qocE(>2~qrqyy zQ*JQ_4HkMl^pa409fStc%o|%$R;nl-E}L=a>c3Rbe}_Rr#-sC0uzZfOi?@+>Pa8y~RS#q8^aE>pzs8hImu3K}RbV~+AYIIrD`FLa-BU#4; zTbzCP7`FqrX&BaBG3OKCda6SxMH45B)N3j`3sJ$+0zAhwU5ddLDeeacsj%F>9+G;@U(d9Kws9zE z8H@LV=gReO!?WgA=r6zxAWTSUodFvp@+8QYJQ1lJk_rl5M$3xd)>Nhl`3cx!IhupF ziP?amPAwhWfzgGKfYSjPN906f7@u?QD4A5ZmZuWW$X~oK z+ZFBtgxH`s9x@Z1C4|uaKVtB(th6bS=Sd1F998qM#j-=T_;ixVM5sn#Txk5=TDIP1 zYL4KemDADwv#UuYz`m&VHmd;gxl2xlf_-aIB=LrnW>IoB%NN_^e`;a_32?dwPlQo^ zlyIfKaEUTuG$WNZ3KgIm)Vn!E#Zxtv*)EW!E6|fl376zMuu@8991imlRa8dnpqZAi zx+;9ovNtQ&+RQQ$u|8vPJc?;99g7k*L`C*mMb?Y60XGm%cIO%X8Au%;W1n~~m-hiv zM-6N06>wf_ws|(dD)y+tJNP6xnH_U()aY(kE3qbXLhzG-ix*FFv_%!kB17IumdAks zh(f_Q0-7gx8x4aB)4$v*a@kj~aYVz7R-q9r1eeX(u!j-VF%QD3k&QEU5)><4 zZDF(FN740jArm=rfypuHEz!s_GP+%{WNeNHhv!tQ2VZKj7i1rzxTz6iqY4fDSa7rA z#JktrSS4__mBcg5kYAOSt)+xhXzWT!9yQLg3H~&rLI&eBjYta3nN%3?`G&|!KXZk6 zv^KNZ1v`9SW(k|LP?e9q5NjE1o$r)$2Pa?N(I(cv28$1nkiEqQiQu_Ou{Y6m{DAU~ z7|x}%Bw%u5>w@PevP$9g3OlluE=A<)lyEy@lB~&j@IRj!O7-D*;IQBr6AX43CooY< z$iI>CxKUG5Hps~Dq_RsmP538e$wBYaQ?{b63Z`M`nu!g?xH+QAY3Of7Xvv#LMk+Y@ zhR*jNreLH#g#LxM?n*U)!(aJJV{M-{*!#VM;@T4%tAnK9s?A&z#%53mH^(lk26ZTr z@?=z5$}Gf*tg+1|Hcf592QPyXk2^Fz3sr6sDQ*{uIwBKV60*@;EW)SHW@R@-S&~KB zH3p}iiONkLuZ)60fG`OQys3=SqV%#8Kbva(pexNk;rr+^1x-ZNi|`3`$V#3A*BH<# zh9%cyq=73H5-(E>4&7x(qn)aIP*TLa7z?~CRxS7t;G9Z3z z-3(J(P*2irBrcf~rys7lbSzo*Tl6Ms*7bP0kq%&285lH)`gn(aS+jjRVD zO+?+xck;m%BRW04 z^~>i5NY)bWkW17`Vx}WL25B$767XGCN|%4k*i0-t1yQ^R?l75=s>mubDuhyx$P!tR zMwe7t#)tLyIGc%oqHLT+!bFUZ6{$Ek&1u5DyAT@x$s#9qwTd=2gwEo;SiD?}g_6VI z7@Al7M^s5S>%;7gm)1eyL6Ufu0oi;zS}Dfn^^o}%Y`!-k9v@f}udiY&cqo(N12^n} zFZvMZo?9lQ5v|R9t{4`J(h_8zcC98yYGdkbI*APlB9n8WDUIAgZ*rl=>BGl?ffj`u zz+4;(H^p4t2deJ42%sKhY;f@C)uU@kDABleun-Wqux8NV2cl85ZJXOTeQCX9@!`$U zU>cxX0F{tnpU~^+*g$(La|i~R@|YU(D@+>V1<}A;+|C3js$Sc=4lSy{8}wW;a@6*W z;U6QG;u4Bvob?2`JL7md;i{<{6dR~%$KrnB^+mib1>qMd0(p_uDCZKRYDs2FwCUZL zD@=scW=4_o*VpuQ^XN>cvATU?kT>Ym$&2CD!`TNhAqV4QF~x~+F+@?n$vqkdRwDfq z-K-2O+-Z~CVIqYwSAoVM=zK28odgGpYj8^6H5*e|`ssJ<3~?RnAI%%k-T9Lk#mwEs zV0pX~<0U3q(H*_$1iFyO+Q}$3Qq+Je{*bj4dyukp)~-dtu|U%A_NEdUF{!i;#hc}l zFzgOzG_ieFH0jKsQ*H@H*$8o&2$^^y-4k)vRn>e^!UPrqZn5@8eL4HaxYwNo>x3WU4aQZcLk)t!%#sv zmYKy6X%I&duUO7zK*P|5>0XZ!Iv*;Z+OIC7NFleQbxWgRr&uyii(&0yje1@t#qPc> zQCDuN9vGK~pGNgdCSIO9?>fXL{Bk~Zcy)E{sF5`ZVF3-L1PP-YZJPmg+xhK3ys?#j}ud6uAlV?w*qdZ|DZH2e>w{Q;c3Mrn0GD1XbM9C-^A~9X^_^F1#An|oFy;y3= zHT1H zn%u~hP=!!QxX3!Ui6u1)7*$xqghw3?5Ba$x3jCr-A%?fD`C9U`B4K)Pa6_gxQs@D( zv!gi#M-x3N^Xf&ZOjlmXsDPME7Nbs(vXbrW7*H$?;w-#;VxBVk+{C12iZST*ykp$+ z6b`3keHcVYdPcG7K%4_UQHtILFBOg_bPaK!I@Gz+T46D3(=I9_r`W4RieWCgq)PIl z7wwKCr6l5>2V~?<{@^^?IUI$hAG}O%y_4BGBp0_8IByCR)X^h*$WNeXH@y9c z(qcVi#Mb5Y(RA@be8`pvf51O6vJo(fteIiV{@JNvFrS9Hc1Y8r zk}KGNx6;XqZ>2jIgIjM@p00?H!FSO7g)N zi+1kpn`(2sjHhz#e%iOewS$?9!C8jNRa{lZyU6NV@%{eiCUt$OMUxyuewreU*xYh)m_N1Pi^>U?VF65Fwr@TEpa6 zGGuZ9eGa<9jZ|q2b)$(DJ0VuTx15@S4I8r5276d>&S(cn<8~VW7rU9j*r02!DeiKu zyDAU-_J&jl#zAW)C(oEYghm06ioKKkEJj!M$eQ8!XgsFP7(;~-gH1*1lvN~#H^-df zrE}EJO`-G)?HDqh>>a6rE}Ul(8%?$hG4V(N9m|UbTbCeuJw*=_%dDA=vXcc}#*0Gk zR*Q!&jItj@0@NheD>C+F+8u){s=`h*I^u|ZPK~kmqe>!my9WI^adzENxG3ry^D)D5 zGq;qGj1il3q8HOMB>S2xoW~OU;bXN!;H5ZP38%1K!WamPh7IBRBN`m+`XyCWzJo!i~Oj;JKQFc!<)0stmCs z6!GH37^jNWI$4@P|3)yqoJEe09^UNsVjTktpL{1B71tB~Gnx3xm-=Zdg*?&aHY6G2 zgc{eeSfq^)jXWB36eTJ)8v_FDgoQ;6q3KXjVpC+R%s2>zMd~J$qIVm=)DbxXef%3L zBl1c%s?V%6*lFFj=)#bar(DIg9?@MSty|K#`LFH9^U~rE@eN{q_489Gei&8r2A9P>963m(yUpYIF^cV^f)R%QON=2|J0Cw9&!O z=7+YVv$b^^Y}X9^=fcBv6_4~L8cFhhJFgUnTH~cjDZ+msDI;tvg%Xs$L5E(!D%vVQRq(YMznQay#Zl8ZxrCS+>OVXn5H- zvL(1!y3y52Q*s~ZLW;>}%*8577d{PZP;7jx6FCIn9TRM&_T*_l z(Z1;=zTL>6EV+zWWnpqSdIOTyFy-)q+}JpS#+MUcDvinGj}A2&)h@l^n|~ znyGCw2$hN}6`kciw*_tNC`n>AoIN}=u-7O}_dRcJSaygM`{1W${;4V-k0 z^ezIuPxWhRZDW1dBJ&60bYh>$Q&iIM9X3`RPw@v`q#|PXunVpocEDXr*Dp_X0M6an z)^k=X8y@7m_zq)^?q!~DMJNJU!RuS*IeQL`&zM3eTW-QoYDj0-zh$UwN48F`tK|t| zN{He4OyUBQdKe=;r!)JhQ{p*UiEecvyXS6?R_q^c0gWytM3%gm%Y5?Rb^&N_f_DyD z3~ywPl4u-9KZ<#rYjhcEV9gSF?)QV5?+v)TwNY}Oc}KYrw^5j28RzD`G2fAFAX~3Y zi%w35ZA7fT^e2wNsyd$^(KR)j){5nUObv}cTJT_}Onb;KmT4#NX1)TYf5~?S2?se=LII+J%`5+AV)?xCqze%Bw;Oq#7?s(hE%Jz%oJFLa9$yvzn zzTO`u^w(t6VOZ{xjj{FAQb3)^xmf&GL&Lajha#^26OivdBXAcC0GMOH-bwlGXG3ec z4jHkS&Y*(Mx#S%h>@QNr-IlwyaNZSDVB<^uN*dr!UIO#QJ=_|`==oakyFF$5v8zhX z@kDB7;x_){=egZ%4Rp>BXTQ(C`l_2Tk>-vYNe#kS?V6#TP13miTXD=(aJkLD>K_wh znCVA})W|NI-e?GBP2-FI2ayO60RRAy9uJzqydM9i(KOj)A+Hq}uAv0(#uEL1^t3r9 z%6s}=5}{f#A*4BH1P5AdpH}kSFtTkbYSiE8R?Y zF#%DT)m)$c3)Efw7r=iF{IZ&6{YJ>O^=KBWXqu-U7Ls)a@#})uf{r{fy^{nUhDns~Wl9ivHT0b+vWktWZjti_- zGql}J#FSBIq8qSeWja(i>tL%=Aiq1b=SFSu7vOVoxL2@uYLLB1*LXr}37hF35)ty_goa zgkl<_qJiIjMDP`K0Hwicp)d38zd%j1{QnqWg!@~fP@0>tJ~%W%Pa1YV+7n>a&$9aB zNe7gK24+~IeLv`$+(hqv5ALm^E`RA{tiK#1WTVIYgZ!g&l9q{c<_#cNOTDNZ__Fg< zL&)0)5QtrUL5%*t5D;*r?qTxi1frSCnCSM8C_pC0{aPJwMf)vSMT!CBlmJoDzi>bM9s>6GfIr`$l2! zrb)j#6e*j(rJi#V&U&8fDeAj;7$;(sD%Hyjf0_Qe@h8Au{u7YMH`4REufRJJ|Fx&7 z0768`zkrL@vKh_w<2y}9Mh_mJxuz~Uii=PdyPCj8NmJq8Cjhhp3@ipp$gu@OKAXMW zMgstXgfrf(W$=2I+yn+?^Qpm55#L7=ui}T#QzIVb3x#J;c!t~d3!;M)8>UGgM>6h4 zT8MrYAa&Pvs^ioV^}(qTT?ZZJOu7PmHX!kU3a72;yC+P#W)S6cb2Fr1$~?KXG+$rf z!_-{+-@vPzPUC=`gHcr52pYZ1kuYoLr|MTO!?Vf7_50Ap=LX)dg;VtUkR_t$Qv_yddCDq#^ zAP`n0iy}b#Oy2{&T`4yQ9Og*#y6`cQhz>J}Fi-XhN1URA`nIm3610vfZ1mTE2%bKr zscMfGwZ$H?K}Tm^brG%HPuXWr%AYCZY$5=#azs1skP>o3pq2!&Apd;0+RB<|~!)-3lwJSI=&$XfKs(c1phA=6| zg+UA1i;ogD%NZFpb7Te~^gfS!7WR}K__pxIlEcJ|s2c7m4WED_SGE^vCShf)++!u7IS3mjCs z{=7nfOR%~ym~7tB1Rrosgqw4b4#0762`j8i7w*vVVb5au!Rsb)1QQ?f3s52npE9jcXMB!rUJbM?tLdhk?Ad75Vlb4C2B*S6PK5&OvsK9hOJ68)Gh2 z(!gzSF9m{F$bsXs2qbSqE7$gqi+AwM#aBHcka!b6C#@P&L{rV?b1wuXs$WL+1yjI{ zPw@Xx1RdBkAPX$_w%j$zr+jeg*P(I&a6Pt66Eh-_``;ZYx(Yh$0yb0&4TvkLeB{#V zD!Lm3*_DZBHTgSCw$!e8$Waa@^v5}HVWM!m6W9`}CWWj}vxvz8&*e(DeI1m0{{sQf z@i6eKaco=16x$=;qfq`~{)71wkZ}-~t#fRHJ=o?q1Zsr4-QmY(8Vm|_y1~%@0S7tg zhO!N(_vgImHCT$M+mR3cBvpEWMxZ!#+T!cs!cP4K29f?^|d7zg(qRF?g*c zrB+$cTdZXoR(G|Q?~r?&x7O)cSjS(O4r+m)^A7L$F-*a0|5sD;3-I0Iu@V{u3WvOr z&hQ+0x9)R+HE^NAo2pBZS@(72)rFTfDSQz9!PrLf;es?CV*eb?bXD5Sl%k zjOCbfg2@y2BC!x9|Cs|at2Z(jkR2bdmSbozxDOvu_>jd3caQkix*~&M6}bOhQs$X$ zbs)lMZOt1wr&$o$V)6An1g!+zi1)-#egb4EtV>knAaU-R!tWAD0a1iV*M{F=?ru`v zLNOA`4gTOmyg(@3_dB@;!Cj%L>Q?wx;w_>(&k!&k;v!W=)x*%AajbC;JbF$(!+ZkT z%=FSnY%OgH?($jM9f%Gpe;df!ifUjZyi{kycBU2Ly|xgL9MJvv$Iq=945(sHtx0}v zn$LbWTC06eup3=SO(Im{*;0LyCb4X#j)}!mJJ3|!I7|RY;{F;`sa&sqNF@%omb1_t425kyjUy5L zHu00ty%&0(>94>41gtmbL7m^)rQJidmyjRKq+%3ZJzJ%XSvN!+(9^akZT9oIemJEPyeOa7 zIYWd4!KA;Xcj~CgG?rk0*eNEz;}cL&v*;ml?jbuiAGjNVZhMY#hh%JdOJ7NrQQ>PV z+a?y?))rlf9jjX?$ELinqih^XdF6?dMk<{HY!MXU`G*N9iI(hr8ROS|mIedS(DOUN z-odC&o1je?14r?NQGnCMgSNV%+@CjIMW`@scbLhgO+K$qg zUY{kM+Os|8!JM~Y?+YR6KlbAqDtCaNIjI&jbtdxdPf5R2liIFF{CD0`g6HhGS+ccb z+mQ%fK!}pbr1tFGoB#fz?a)mUSZb+qMWWc;pBQE|3Xd;(`NBuN<2pkUu9c0KHerfp z^bbQqcY;;}w$)=5ZfT^Cn5DD{Rns&?I2YaWKvM{mc z$(o9$-Gu(JZ^Gc&l`s3wLq+PXh+3dzRZRK(A*iL65-TfCsDN}#;D^cdK!LR2yq zn`uNN(c0iKBElxofXX04BmOYoa)%fr?)y^r(ooGu3)36h5k=fDvw%|1QwK3PKe&?+*rFX8?Z%V9vXz30ZChGA z^J-$F=TbH^o0_93+)$S^p?6_+eU$Vmr*U?s-@U};#r_ebi&7#yBM&tHs=tTyy4S=F zIxYNK#XBD9)GbmETtE~w?-q5eb(4wO{DX_%y+DNcOOtty*p)9ABf&|EQ`a|uz6=z?r&ci&wX`GGSI=`V+SE!()M)ky7x9zH{1vkg;EtD z_r>Yi1v4(5rsv#5p zjkN9mey?@Zf85VDxlc+wVViu36&`gdS3t-Om}x+1(eOt$xgw7HKXJ0mLuc8B5`;RZ z7C^y!f!E8N-ey-E7~8J>Zm9A^+u`YgfX&aOk`5ve#x+n(y}MU`mdb4yB44vUvUP2#qR>K81aSK_lI7^Ebzbb zS-9$R!G6|q=#Mdrxk{zcP!^0;a|C_A-01UG~4BaLy0zX z=}LwOjEL5EuN;@uK#Eh6Q0IJRid88_?U44rwwv4oSYdQm%?@X(de*oQBjqByH_OyA z0-O*$mM8%fLPEjJQ#tl)tSXkbNC-NFGGm^`J41{Tp<8ZYpMXd27>KQL&TIDdPQl2v z*W;LrnOc_-=S%q&{lxtcGcRvq8lpn6nujGBk->x_8VPGhUmgXF_;~Ll_Wb)z2(+r5 zhCiubwo0|H{|Fy4Vg&h1iYo+~bPD@7n|y|FWb{0aO-&6SK(%#Wvj-=(Ip@4Vy;?l) z9@1Xt#?3@tviel;-88r+5qFG17Q)hjquBG~fLYXUb*9sx`Gn*CL}Gbd72DSstorf#7-iK(GFerMPw zc-+srL;0s~W=oRjTuvDOfn+MJW66Me1@)-X3#S{U2KjxtR}|q8({~%RNwGippgvu} zD`_CS+GkYLH@gml%OQRj2ET7UVTTCRJ>dNWr0E$&inr_|y~mAA^sMe#o-5KiMP%YaDQA8(AWDx2m~>a;b3oSoOHsYd`#}7W0>67!xo^ZRc=he1 z`epCudpMoIw&2p8;IB&krwO*r(E=mh&i7TlI7dCCMh;Z&g~{n5m6`>PMt6rx%zB9u zj4ZMqkzLDAz+d-cP1u1cy~LefZz>=xA5Z>B2Q2X11@$h+6Ec_QOR46@_LNh}8T1LT zGNWIVo5JO-Q-;>QhcsInB!b9>+PnA4Cl)voU?y05Ol`@(vVa|Q$rwxwG2_uPr8IFsDFLXSR zBa%b+(d!Z}yItF=%tqV;==p>Ya8%>^{ZarwH6eyrsmd-iYSt57hPO;Ek{atKt z@FxJp%~=l(B|eNZbpk_fs)p5t=ui+J+FI+aeldgE@Ua82Q0RgFC?kLm8Ic)zjdZ!I zN|(0>HE8)0@J{@s^a)Ve-IF-LdBpcI*`1SIzew#x=}=|Q4>jfqPqwPr*mccYlIm#R5OVZ9}!>k4eXBL zsE8O%qMDWu@F(upDLA#l5_t&Wv=o=@8{BErg5J8o?kt1sA6BxcYU?@eUpSj}po89K z`UXXD_n>!&6l6t6yY_tGKX6i(5F|2&2*jpNY?qb{?<%?MM=PSR zj52*6m6g>(1l68U!RiI8IkS7|VLgM%)h?LN0ez6C-S_!YVlr^dvI0FYI`b>vuFbyx8*NL#D==eK;P7o@z@vK zky|kT^eGH?es4R;Mkvxb1DrN-7%$@*%v^CH6oR>(?)-UqkHU$vXhE0iUdf23Z zcXuwg3t-PHB0)LJmI8bSR8~{;6vH`#5I~^uB&87&Fm!B?i3EmLgMvljam4#%=5&o7 zr}_)qN8NY}>!?27>Aj^NU*w^P8Wt6vm*vpJJrj#I9M2et*zY>ze&n;+;)9`N6F|U~ zDKdI%V@SQ(OeCRCj}Cn<-AoRTUFd{1zWzZDPQy`DL4M=7#@U)b~X6qIj`Z>&LibYBBZvOgWpFvQ#J6NABp1>O!u|E zVZJQgNjtQ?)4Q7^vRmT4uy*7jQzTAF*U0muH{h)le}a_6t}G^Uc{yA4MccN*ziIx+ zG~DiQJZ?~BzA__Z0WGgeI0PZ=<0qg@=ZnjrUvP~r$+Fsf@??*%cu@8UaFHrr#H;pZ z;vn8a(glnJApX&wLh)@nUJ)iOQg**byOlkG`azH(TOGb5`S<$#C={1Z!>ZO1X1ALE z6MzJ@Fp6~Fsskc*8+4H_p+k{>tDlKVzHksah7okkSMg4y?yNiA-u^Y_uNN6NnxsHA zoZ!e)VRVO3U4BL&un!0mz?ZH21mKw6R0QbR;4X#lt+4MXH~&gT&ICce8|*MQ)@E(tknR$cbWsdbhY&eb~$#zK~8mksVt z#no(c@$^fkcrX^TrkJ+&3E}R6T0igcJE8fG= z|8G9bX2wo%UL^m{rrDJ--xG!I1QzyU3VGHlbv@sTUUOcf<0n9Fmhz0~pLNwT#KNgB z7xV9L9Lc&rA+Cd)6m{eX1GX8QMt-EhG3#r=Mn9B2jInv$XpqyiD->>uO9h`FrUW)V z`U=1wzc^stu>Z~aCbW$-*T+EJ%6bQWQBfec7TCBK&SskH_q|SQ#Gn8tyH(gM-7z;c z=L{tZc!u9R!JboQ_ZIH4pmIobF*awI)Y;2K1a&g8*c$IJ5JlYcfmAjS@eAQG4N^1c zl)1p$Kr*x_iS-E$mo8tTkPk~@K(4rVLtDfTZ(@rqt?(zAnmue+nLL!6LTIIbrHRJZ zsyea&lVz38B0cly{--QVCqUIXe5=*nG7@9n`0(N~2Mm_yCB7368u0;Q?F(EXEmn{(OebXf>W z&6vpz`%&f>W8&1 z0g)K@8LhdkrxAcElve|>`b|V-p&k)oQ7Gmopqnk}&;H>NOORM;jn=Yt{&R|in`VLC zoyG6n@9u`$p6Br=`8K>4EOzrUvd-7s1R+z8PJ*uctOCca%oOzzGq;e}*Xmg)fbZ>& zGK;eza{9u77EE&L87}$Ovj^}RQIZD?E)P%9FcTm@>xZ8t=M)-PA}UrJmD+kmhCVCeS?z<XU9M~((frK2_n*uyUjmhdRrk#=EU1S-tlo5z*6>EINY+1L*>6jS zjyXwbjkv|PGm+ua|B)S&@9(eYZSq6^1PP({0loKSFGG~6(=8WA%x8ugm^R_o7# zLh>x&jpzSIa*v-FXE&e3b|_WqmYwco+u@LDrx^PJ`J9GDyRDNL#Rv|SAZ4*+ zv1y$gt1l>SCUZ3Shg?MQrH^tHL`r*;P&p4WvW#E@3(f)PH<{&}N@<`%r$XQ_H=%Qr zWkdF}IIq-iKELN)R)+(fn{zERmY*i4()C3|^q&P&^-<09#KFwr4`N%*ft&&Gv|ahi z5=7F-P|#@$!$yXj421uJe>M)U|=g;55ftYUyZ9ND`sa>vR9>(JLMuR5PpR zss8TjY3*V?0B?=EUgQ|vhYR1}s0 zWgbt7hfWU{UJ1~^8{5p`gY8g}rnW@KJ)imtytKkHuXb zxK2==g4^~uOk+w8Kx!u1HW4V&B8-pb;$CY*{Fi?nKkbPR<)^Nu^3&Pgi900qQ$;Oa zr@Zlfr$@|x5S*hKI&x!-JQLp>WUBr}L^3W}J5fW$$%+)bY1;Xi2x?R(yW96bZK0m? z%9k*vUQ1~NT3NUzUPhtbMWf;zK-8j5{b5mR_wS9Rbt-t7$p5`?%6h!j_)rCzhnu%s z`(nH%;k2P?X~k;Wr2he_ubRG!8-}dKXM`|2>0A!{SIJ%^GV_Y$Mq}nOm>2i(d)r-p zcx-SFyBul|a+cVB4?z@yo;6ROVnRwAyNS`d0~ND*a1^r z#MEGWA}CM?3Sh#r+VKuI!~I*z)M1?r2Rk3_*9P^Mql-m%&+Y|#I#M#wEjijX`4WEJ z$-3}h3?(ydri?p08-;Gy3Wjem1f!w_2m@p-L=~sZc-D!oJ=DAcR$UO=pow?v_30NO zT&W|YV1}n;)8X~u=K`cFGvQ9DI^u#NWE}}{h!ExEfk=5<$CiV$_*{dj|od=c&t)sh{5e)w_i_T;JF_M2d2d75FcMkhN z-&n~rcKw!j{1+8A<7@*Hd5r*bB)Zs|ckDCqGYDo88k zz@l|yEgDl<)ek*Ud@~PPcUni3=db7G77nG(PBcUh3OJZ^bnc?IMCbHP zt5=YeC?PeB`&Y$L&qtts%BU2kEs(7rdICY%Rat$poZo^97Q3g+JGgN`X6WQ{$;aU= zk_XN|VU^hc-9AIzq&hpuKUF3#}D#|JhQ=uaK4MUN*5t!rK{pU zEhwaHGJZ|#{m(ICntxG@4husvFG&BY9~UlqMIZK9Pep&^9r~^|6=5v2xpvj!Swl8L znn7$#4IJsH`iL=+nC$x?y-_4)eu!#*+U9df>fu?uLE$@8B zk6QGX+m|=IcdeHNjK|;^AOV7);<>aMisKmwh7vCv>XFfb5GLd#<6(GGuuq`tZM1KX zx(Dh_epr84A=CVyfbMIfdE!>IR{PoE-Hz*3!oFk6_9?P#1{p~_OCtAFfPVRP@RNnZ z&Sdg111Uwf$WUJ7pi2IiT?xd{POw0bEsRqe zk>k{V7p)N!jvfu1V$Pn4#6}2gy1dYyOh$3DdTi%e#O1)(u=qqI5uARdn}05Y#?s6x z-%%hold@;j3JDv>F0YR3OdAgG2=kDU!k;}imUb#Yri>dgo%C=6V%%d6xwPNX}WG$B2Ce5aqY7Xb`05gr`#rdL)`3Y06h7j zU#}mBaIXRylY73L^{(Y}f9fo&xb~R0>pLH>2k|?D|K-0#CIP$Ti?oQ$7Rse>5X{4q zDFY2BCutQ#x2#%)*yz>iraVKiCONd$BEH3MzJ*z%csqL+J;8XB)$|5??%~{~ zlY4I@Hu#K+hM(Z>Ugux0oBtDh%dNjOQT~5;dhd8P)c1Xy+Q&$7j?s`%>-035;vAwz zs@1AeIzGp!qH3MCNN5oviB;oG#W*M0V|9x(+h+_3?^9ZqX_XEx}GRzkNe63LOT%k`puna7VQ?|1am<2s*WjW@q&3^6y{0r>RA%e z$^MYh$9M?cb`ylbgGvWxKV+Fo6|gX|VYE5nc!d`?dL7OMXYmpY}4;A?`oy_>{@3pXMqM zYtFDXN3vYANAP&10^VGQP{hJX<2o5QN^Y*6s1wb`!wYd$muckvV?SgPU##K2etT@H zcGGjobTItEHxre(cs3?b4IldAY;A(dnfdUF_}fc!vIEp8yR-!zRJli;VawPxp^mgn znnj0$%G1{JJ2a9T-JW@>Jt;*>&2l9wYOG&Ge_Q>tN7&fjOc5q#P@BI0xzg#fEix@zRzcmI&{dlO|;w%n+Y)h z5=V8@|02w9e}rc0^P<1O8iTK~8hLm%Jx`kh?N^($k=dBwZhnC z!~y4p{K5QY$Km5kG)G`}G*MzG1#Bva)TD@cKDs-Z5>D(%&RLZweECbq*W&eJqx@HJ z!I|KoI-IRIDBt9>${jV`7r;YQ>I7!p@Oo$zD~v|d8!N2?%+I|Fq}8y15XWTXe0M$` zyZkNr{cbl+>17VU#}(I#x^_cMASE`b!Tdg9L*#lT_Xcotbt zI6UMh`1PmvgxO1Nx~+D5KV)uiHCqJLoUkHZnpzD@gI})rA@f>to*pfEEU}(kmFP-t zbyPl4dWEq)HMl>plj=Dv9hwZs<@>`#xd#@jzxC%6m%%BH;iWO)>YMrq*+?aZU&&-z zGge~R8)=A)*$(v$aTAW5bZVLr|!;Y!*7 zH*#2vz}X^IC!j6XGXtFk@;pPl5qy};*m-VYg{>{s_D zP72_M<`N7s)T8$^*@iagiw^C^14R?ssfDAC8HIc&ZzY?FR{@=@ciIy?UJ6!mukxeQ z$*h-m;w z-)4nA1*Aq%xMjkPtE)CJ5Ty$Ipy#?l)rExrqPhzDL45jYsT5~BVj3MSmknAqc6SmFV4mw zc4M^D;_ZJ@F6bYkBsU8*HylChR6u)UGHFVVJF|tTuo$5k*r>yoo(y! zh3cK6wy8;2WkIy!;Ib-tf__=@kt7S2c^ zo=zg6W|P6HE5`pDc-C92uWkl zqi&5ljC?)S#&rs8LEyXk_{mNbe)33?eEO(WH6RkK>utOi8h?&Tc*-%q-5hhGiP1l6 z(diLz?KDifI+Fw)9h6QJ&cV41P^Lf%2=;-2i4q?$K+!v)!B6VYDg)^leztgU?W=R4 z>b*$AjGZd}O$=0gFiQn@JlJS{-N)NCz3J`xePoi~64RpvMQKIS+osnPJ;KV*W- zULNgx-Pc`ruI``g#0TDCukJ!4Jc#;YZ(z`wp_{@eWT0?0-x@=HV|yC;ZQy#u>A%Wt ztqmOH*FJvI6vb{2B{FvdPYB*R#f5SaEeya_He+=#yzR+cur7=n`D*m&jX)yM*f}D1 zAN43)puI3;2p3F#D18HAL6`*hZra%K%w${NiG&9HGtiKVGgCW?0A5H5bE#~;MwxE& zm=95kt}s0tFw<#S`lijh3+{(VxsS6W1G2HYZ`hk*^DUuuUCqPuVY}S>1&6|2mXuO+ zd>Z_d8Dh|LRwLS^dVw6}Ru^RMP$^9D&wef0=WbeD;xXGh%e<`B`4K(dK8t#VYJyw!(-iOcJ=|_z9#Je{boMhGu*Us8p)8Du>lmG4nMw0(`N@pHp zuA@5-A#z!MAE6}88OryW+YM9l^JO}bfGd1P8|-^5uOu6FqT~90>xsGYb#KU+A$&Mt zPnTRqH-51n;n#vKy(QU}Ksg2Q0o<5=m%n^qdnh9TUBIk&YMA5eUMOIYzZgQpYA zUsSNK@WW=5z*pFNF{KxF@9`3%44bm6pQr!CI>L{K35Uh+Diuarb#Y21`78FP+=|AP z-fV4*Hci9?nqw(f+w#$*&L1*)q9#1AYJ1__7x1p?$NNhKKUY$8^qCU{E46y5`KP8c zWs>vdV29M-jV0Jfm*7Uqvo-Tjn6!eidn0!%Zko@gx}GlhV{yGz3yo@ZJ06O0;YeV~ ztt}26>4306FAy?lOs%4WFz<^nceKgQXzD}8r?^3Z2}@@`?j|>X$*V}5v*tI&jB%)< zg_s{Q4Bk;TiHIASl#awZyqz)wy*H&6l&yizu$TGT|2@!Va-7_(It7MBzQIV1bXN17 zP%^>Stk1_CDShQ{^oWK($D>5D#g~Jq2hwV#KlAPm6B(`eI6qgX4s&6s zy=--{-+t#1xC~pi>mo`kBnZ{AoG+EWZ~EgRrcTuJP#}sx)U~-EP}QUA57Re(YeVBj zU!(+`QsKPj7z3BrF;1{|DjkG{v`1nXt7?6fnJ@nXJee_<=Vj(cVV_(V*H${Utm=-M zv2R?s!I}%(`$ud4VmrFvc|o0af2q=&VOm4iq+hy`)@x8nVF8^KgO>g5P8{U> zdB383HCltVQ4_u*I!}#}-;Z5MVYR&wAl||Dg0kNb^_aIrS(+2b253~=ky>l&%L%OBMUXvp$vX`&1BW zE&Dz{PL`9n%A#h!NuAH#C@contTI<*n_v5^HV4*sSBxozf9~}4yYZl?w~I5_8t6ln z>JZ5{6gDQQ4-#&%1KWfDSi-nP6#S48>^8XH0E@!Z_ANUb_b#}(|D4%{A@QZxf=Pl6 z^FO^uufFq+9SE#gGsZ%syW>$LOT0-%2g`Jf`R+{#3~&!6ju&uCyYq=c0Iz@ipMq0? zFUc11C9|A)*cA>~S>z_CVf*huRUFuABgO>pgGb>1f(WcY7IkXbmeX|gi1Xncl;JG3 zY@EK6(CM^7j_p`r_YlxBN>-MB2oWYV`IRK%aXxLK$qjt*Q!su5i{FTx5?xHd-F3Fu z{hJ}bs_-v5ha5gESrXxvWxvDMq?akwPR1%&!&rPSaBx3L>Xd13{H5`c^{6tg5mzQH z?o=cIJycA&UJx}bg8JW$`;W7%HuG$6f@(%`7E_dyd1xlmwSf%KA*g7+j-zMY%yMDEoyXKXUz!)5P`25d-K(kkf)%+Kk53U*z9+c?Ha0WWOEGZi+# zZJO24q?7mPI@?AT=Za4TB661ZW_tqaK%DYT)y?cOz?Iwfhel_^YhR86UR{5b-et0> zqBPNaCwQWb7fNn3f=IEIsQvS;e_w<&M2oL?AQOdN!9;O+@E?^*Etir&1w(h3w`9XH zM}`jUzS&lZrMWd|QQ2dXXm39aXyNQ!d?Do7f;Ru|Lb!3v^Vi`hPO>r%1@aQ^j#aBd7!V_uurl+n{7~Z*Tc>E!WZmRb zc8VW^1^pr~EE+75m#sFjnd#7ds|vnzW*u9E+{ts;6_0|khcA(o#fX=<_fT)~Q;F~T zdE9|lYov&h`1RC?hf$=nqVjdo2zwYB=~KOhX2P^(cIt<5RdG1U51Fm~ftr`qy!Dxd^${rlV+HXhv1!V+?i8ces=SUR7NY8U2vi`+I{t zTrXMg)YGopG=3F0jem{_6+bDc+5pRnpZE_Yclezx|2^6Uu(WwVvDw9}>&-%!$qkY8 zU3#3f;p*4@!zZdlTtJpt@`?5=J@0$l>E~7WADG*>hJlL1*3mQpUXl3*CO|g)> z^X_@QNKWkErZBqM8dl;P_obNb0A-1g)V{8k zQa2^9s$&p$K4qJo6a)^V7@dDf@S0;S@9ZG^9|((g2l=WT`W&D$bjPp0sV@(Z@D&Th zX$5PUwL6`k<@R-Ok2+V)t-xn;zDqC{Ry)Lf-=wBgt|6JMS#&`jD!FnfIPsQ4Y`Idc z%?vhkB+6h@NufE;PgOoF(cb#X+|%upGI{9@ z{_S*QkDhPaEy-nNxu_kW&e72^V*ITkid%mR;7udN(OMajE}f$|SA1iT+Ri5k>W1w^ zC5d)kdAzzV+95-B*JBK?eBiIX*U^nUiXyH;efvbw+qP6qU<5hLx;{UjL3l-ZZFu1S zh;i>fYx)14eVsAt-zx+gbHdXxgO#jrv($wzzs{P>DffR&X#M0bdc;5PHo1BKJayNg z7rgpqZ%V4D@8XuBYbl6Yp`_W0WSgE`7GS)A2N-j>v)gj2e>>-j~iCE z1u9MESfar?uN)|d`o!^bhPt&Z)h@jh#Ai5K4xR+A^fL?Y&9k|BLaVucqBlT^fpTg( z;(P0SK;vQsD{xERz*v*t=%bY31NycFcyp71iEH=@>u)#ugYDQV+RMCTuWBz>`cEa; z@I!o)r1V}DYm(+Bs92qEN=5&3$97SlO-YpRB8PlszF# zO0qZ6^TtN$kW;sWk@BfSmBPEGlsA_ z&?-~}>dZ)O5M>;?aCTug0HONsq2-we8i1j`-@4bj!i%D|x-|_xtW;XE`G@$R((NYM z5MJbXI+2RFYq1*L9%b9Ryk+eMILbfA0p%7vD6NyLD#?#3#Hu+S)4|BSx+krIf5@P- zK-aR|O{;XtCfV*)YK?+B20GUoqsg8fT7jiajj|2BK5j?)e^Q!kPN;;+yXm8IjC$Wl zCFGX%4Y#`zcg%yG1fw0In1n_%ivt$I_nWwb2YwZ$)svNSgqke+A$x7rFM}1( zG8iS{>D^SQ54F4cv-@V2?qK6k&QaJYyo{qDKixrryHsR;)?fwYjV#H#XYS5-mJ!jV zWjg(pZrF+C5q5s4KpE8c38$}Yo?_P*GMJRIb*(XczcDa~_R_FCd3!nt_~GuS2?`2! z?m?k_WT6~B0>bcqN$}U~)?~FCIGPb$7FUlsrKl`wD?Nu)x?q{>4I~$0fZNkTj4O@! zW$wA-Cc)o8X=2RYU_X8E(M%*knBHYjGlPAE1$qLE;o&0a40h0_6QtMiDSCl)4qpxS z!M?+s-X}dEz0I;X22ro6_Sv}LtwR~6w>nb-a{nL5E6B9l?L> zwHWy2?*^+Vu$bs*ja4PZWuDHAy4L^%Yr*tw*7BPM83B;Jb)Z5!Lg}A!W~BZ=plRj( z6b+Roifvp!wYySnCQ{xN8ILF=BiV!di}?SJ>6YtGk#^g-(F;3nk&VzoC-@M$(H3my z#$ytna+7FfCVd{gqn%0$9GW3-(CC2IE`Qkn6IZ=6iZXE8AhKxjMX~d8kbY$V%7}hw z)VJ~_M<~n;wXAb@b8y_r@qWN)*H$zWD{UtR5ot=?=yrTR0cnlD=IqaKCAg1rG3{P9 z8R?o3mJ!Pl8&)!>wMjB)BDk2AHxom$0RRu3k*f-69CYFhB11}_J4b10eCr6X zOwr_%$Bz>&k6w|A31#yg0iY`s{L zPt+a|!oZ8=m?_03x ziVn*6<~d_h*@bRWsby24Lojn&N1>C<8nqcWcisqbn_Nraw+D$oJa2)O$~GA?h;CqZ0%RgvVA6o-6{l>|W>S)o-$*gqjr(5w zzRYdmTV5w?oyHqjZxixK372t?zuDM~UYc;&IJiI`Bm^+xk}&#Kqh5bBXFmlZ&VXK< zRS^B@vI6q=l_Ui=xYDM}Hc>e_?*qA?kyJUj2s_1cj2rcbq&w;}?T3t{85FMQuV{rf zu+kqui(0+q{y_iU^1YXyG2R%0$(Lf%lJR54gV&u#$G-oNsg+Y=3~^800(TReeMWfH zHx|owJ${wI-`4d<@9{33Y(vVvD}(GGFh|?<7|jpM-m>gx9&a z??B9PpT3EB-5cjL)9X^|C5y2V5sO~0NnRSbL-TyuM7|sx@f|oRY-R_PoOi>}Y z#+~)YPo+z#^N%M)csU-VU0Zvpd_|+heeRcmn`;E073d}=)BtY1~f ze|j?I{Jn{@Mns&}qJxTMdT%KO1YCL8sJFyqrRc!~i-+T=-Sh0^s+LCY*ltUovop*r zj|aq$4$ikZ({DmzBy#Dank&_BPs~43SuavxK0+F6()<<*YsMCgMY-&Hiag48E$mzkh-*biRaWo8cc_$Ap^T-*-uNJ%&)t)eQJ zE06-4?psj>4o!0gD{0*eM=jD%S>!#aWaU5LefVYN`9RyFUlOX#UNcO(iwAdxDnC+Y z)Qjql=-UV+aJgpgk&lX6-y8$nm0%pgWn(x@JDB`IpumJUGo1c39KD+x$V@)&5q z?-e?-C9CC(_LqvL?ylX|+6`VCGMPOuScolJD84k)9$jw6yWcp3#%V8aY{ACoh>nl^ zd^eM$_O}pb1UFTPKga9PsQ}yo$C;_tk8PYvF zAOry%7FXnss<<3?4xHu3dwRFIGcc+o+4nZvzTsPCIA)UOIJKe<=xBnp}5CxCW19e6@t|!>Z<@xP*X)^&3&Ej2f+rBc?-wYJU?n_%fd8x z<)v6)_o#QJTcyM@xA|e7MS2qT9Mbyn&z&ZpmrzH$j_R8hc2H)1-Px|EN+~_F%=Fz# z;AFOEGVzCu8`R01!&8s@oSXw`q}Zoum*z91WgH!70R9}gg9qellWUdp4r5c|9Gbo) zaEA=fZDQXIHR*`27-0(A8aCgs9HrcFTU=$$yErmD z(`=N5_=xG#UtOCWIsNaw5;ku{l?-fkguY`vPft;&M%NT|gFKs*%G#sb9S+B~SNfxy z|7NgoHB_9k5Ut_Rk~GCfrhfDBF{^RtUSq*$?h~>4?@l3(WasS^trr~qzHNuFNEI=x zsnJc5s-wJmE&;s)gB+-6HBGw0W2IRW`?3h&=wtMU14cBk$2 zI=P&A(b34NO7yRwHSghmKfWV>Xhb^En5^R z8(P6Wh%@JdK&*)}l*RkA@G|mO-jrR*IMm0;dGoYINkNGe5Hl!82qrT5hm))RGG5R2 zax&VcY16@uj_^Df43+|LY$DTl$OT*^<~4IHEt6OUX(DWikAilE$yCd;{re~H_HRr7 z1q8GyXl1>KB3i1kZk8Bqn=(A>E=EW{;hEg1XakJ95k+y4@-1 zY$dYj>qrUHVY|csy3&Qy&N{UuxZL17wSV>u#D%g{QqJ) zx&O)OWaXt=IobawrZcuOa=8+6GWO}|w4#RpiRmu${-2oc(Er4AENAci=M{|Er|kCx zqJN~Z5;-KhRu7?Eo~pYPns)&Z@%pOfb9b@pD@4kAyW+GC*KJ9my!>svX>hP(Q)v_y zsZ6Z(urS3ZG|D4p7%~hsF=NhjKd3Fy zIb3=zJI-&bJ)%oz>(0rPdrX4A6^~-}=q|Y2lNGbUWKXdXYb<7mGn7Elq4USOR-y`; z=`n4kDjR!B{cNg{s%<>RwJ5W4n!+79J8mwxt2q}tb?g{IvCfjFvF^muElP7_!#Yx-xe@V338Adv+3t&z+F`SIN7mEzY)Z<*qZ!}JtS^`*EUKDUN)xwI^n~5R zm1wN2lDhL*QGSvFVVh7T<-lt;c-xq<@^##SwN{G$y|?S$z4kO6QBLSXhc>-4$JU#4 zq%J+(;r{aKkK%{aK4KlbZ>IQ(5DU;7o!@9p4=G9^C*mgyRmUu{Z10sDec)mJ;5xDG z;Y+0NIDSV;3b`4-lXOsLZ?NNvm}~72X7?vJD~_Z5(oYp@azYcU4gc{hT>qz$Ixsw* zO24UHcce=-h-$qy*Je;|gLU=_y@_3jN7y6wy>YzcM=O1XV^u$7)D|JHykjeR#=69~zw}<-&G{)hb4yZFm8#tWK78)L?J~q7y4H078rS=FJG){J{^0er zsR2Ts43w0tIv)fLT2;X%`n@V4=9(+q=teFC0Q*w*?7Q>sNe*r~IBeVREX28mo}rJn z$|m@O|5iL#;MR zsMDi{XOj7fcQ;jS=pX&82RY8w(KMjh=r8X8ZMef{Zfa;~cRo`Ih$8aJ!(LV|+D*fC zeSXN)?T<9{I=dbO-4knUSD6&Dx29I?{?A)K%*z_&F6i=;?SWWcc^}yHu+!YXBlPUY z@XFY&(2jXUB=|BIr=8a09~#{r3Hf6S{%d8x=yZdm+Sq2iu4Dz!8_BBD-lL}}i)DT?hL-+xRkBdNCCnqCpeOT|4CHZ5nTmxHSLeFMyyLq z>wnaP4iO8^g$0@qy=S#4+DZ-EKAy^vg~nM4(S{Dtl!rxHD&G3xAG^HkZ&IT7va5R! z9I|>J{B*+Tv=ITKa_qp&uD#}kE~#{~q;~-KV$?<|0<}JB3ch_&&i`{@m^2JRk$m;b zjS(kA)u8^&#i{o)!|%(e_VF)X`#D_qzl)gddmxNmx+q_brhXzLo` z^}=}Nr?{tGFpvu4WB}w;e|vL6Hdss9e>X-EZC<9JUO5y-*`I{Z&aLg$?!MHY`4P zr&y)?pLifd;<*B&K$@1fT*!hu@^t{&L4NU=xiU-{U9t|!0(T!16fAe3?^;wpAA}Cw zY-aOsZ&_`Wgc(4)&Icegw1wVi3A{ZB8_+?Buc>(WeahhvnqUaLZ7z%LNQf08W*&XMye(d##QAR3X^Z(k;5dv#pfEw-9MqdnL1d`_h zaE@A1-rKkn^p|x?9cnT+Pj|YMR9@moE(fV;_B?n!D9q{;^s=eAl3gaRQPrlRJwqmoTYSgGk`Y4{a*$a^vG!7Z-b=g21uZh>aNIhS6Y{&lGd)g^9 z88Vl>NuDq`5*3CBaj{j-n3~_bypMn-oDQ8zdGgQ6R1OFJYc7I=10vpFKVJTj@}b_6 zNwi0^`Wp_SS_3`}`xbUKDSeF2p*mCJFL0tM5dY%o4tG}L&lZGEi4q8EASf>$;83yl z1ODLz|4*^S?${?F7tH+pwAcF4j2LHTb83QGLcHQIZE$lao!K|==WYOJn50&B?@7;L z?=VKI89mD#rY4e&u4&ya>V>9gyn=$e3ti%&!1;&SZuO-B(U`6Hgh-l`?pw=GXU@Xo zeCL=}dw3348Mv760;{K}N~ZMfZAbP(nixDnY?T26rj}t2EUXXjtoxEGdM_ANN+{5A z={S3Bn_zbq=pD{Hvwz`OUFmFAJ2Bae0WrlOWv+W99@&E5^`C-RucghE=0VyIVw)Zk zr|+{k;I^Y$l#auR4kqNdX~%j;C;~Lbc4AQ(jx}b@k5V@xfNw%Z@3HnCkNSt>$@Ub; zVpB)!KTgpEWXuWx=;_)h8wtSW#bnv+dDI7IU7A@rC+h7e4KT3FxcyuAeZ#Sd%#C!v zQoos?UZ9>gC6tlOnxI8xgisPG5Euf;bzJHRJj+%E&X@FjOB~33cahc-%mm0`6_)DV z%yJjkW%w?4k}*-|pPgS00Zj>s98K z%&coIRgc3o7QxYV4>Pdqn6vGrA2Fs%$iEA&b~n2bt4uNVJe!b{m{5S}X$PcScN9;m z)YW*b*hSQjlE{rZZ{)_f{bei?g_7r6X?yt1e6jn0-)w;tb8+fMRn?UJS-c~qKnlK@PQ9VpnWUWe{~>)3 z^j8Q#!2Yb^zw@DK!gds_+E!mS*cOI(6Em+Vo&{axKHbRZxdvI?`*(9Yu?Lyr7y$C2 z$eOh!lINUovMX})D@d9)N7Oi>*4vRvQ<`aJy2jY}p&OH#m`d6B_?X|lDZ_hQ@n?^z zR~w5YTepEvvi!exGAhdFB%$BK?AF~@Zy9YaNGR1`oNU3qzHHn?wp2#hvG+@AE+*LJ zw+Z`=-n2|;DEHD#y%fN$6JO-5{E)#$#Jm7O=tjvjngPc6Kc60}>VL2~YR8_wR1{rI zo%|sabs$X+kMoEt*Vyx&n~oSV7YlP*L>1c6u7CJLWPI>yqy4uIq$vK_qN%C?d21xs z;Z}bndHCQIiZ<=CZgRg_oV%-|20z(5O@1|-0OqN#wxK0H^x|;}>D8FDN%=AO?bu6v zBfTNqZ9Hl-Ih#x9RTlldPrWla`{$`rW4@`V>3jZTE?jgl^dz%mI`e}QGRP$D62Z~f zdEU_5gL9(aF0V~bml7G4qx|H^sO`O2!+8j4lZ>JnkPfPDC^s$C?}c* zR9(HDY4Kox(TUllsJlGU7NOy9SdWpKbYvIG)s^N7aW#*~`4}YZ0b_t;OYJ-1QWzZl zx+eSchqB6?ZIcsyH&V|#nTT{g>njm+u}^=3^BQi_-ti3mNv25)_f#(yjv^hR8=+sv zMyi>8PvYVEuTBL$$xcyrrpLG&l2?CqJra5|r1!J6ke1R7qSfgnqEXCUM+xA0#!O;} z=Ee`1%^xy1hjuMdo6hU<>TBEKf>YMK=v8tRDB@JwWGggwi^q!>2fq?s!R7XqaJ~qz z96O%(V!)oIWd)J~X|sH=NCV8*%_W`?%^dezKh-CjVEow0RNN%W8&o~*7bGW8D!oTj z$Xk3&J+z;SmT}L50TMzflclv%F@m|)x3k%>bYB*uYvk?awg z-CH5xlcPf;NYEo7YGbM9v&`fb%1A{wq4do!&=fn?Lg;7$&G!;}ibSrHJ)6qIkJxWC zqDm>VQ>#Rt%dVd7c``F$ieqk+PJzk0#kKKtAGQsFMqWxS}0Tv^iSIqeoyz)V;hG1C+JhZ;Tlo9ev3b;SZNZ^9gyE_fUH| z2;FmpaJ^DY!T%kd$z}j;!|N+;DepyRr+AiEr*UQSnujPb1hgBrmR+Wj-SB3(VBWht z11}ZI|4Xi#E2;8ki?W!|l#fYrfLBV)z zT^Ch_MxBB=h`>6$?(IPDyS%Zi;H$>Hu;DiRoFQcj$OjQ z7Tn--oY>}ZksKfogiIO&IFAbw6BE1*2QfqQ0*E15O*b)NRh#i?&nmMjQ4HQAbwxj_x>Es1&N*3Wcq&dG@KAdO22}undhxk(+t{KF zf_r#zs>ggii<%rnpOn~iNsZf7AqD>N!;S{JUk$nQQWub$mh6;er?QVSP92RLpYta0 z8;80l*)d}OO9y6*2dR?%T9KdgP!AKp3caB z`ynHZFajbk=`6xqF|=M1Q{H?F;ro(RS{zzz$r|3@B111f?mI(j-#c+ncA;} zZ(ySeSGdG&oLm(LFF4|6`%+TIyrqeQB|Ns1 z7dMZs8QE|S+6&L^ouBVuiHQ1Wspx$TNCF#xc>Ki%DEn)6oh2)Z^j~mTyK{~8S@)lz z4{|S`Y;AsySJY&Z*%U~+;6y9_js(U}j$P0$xW0d#+UE7nPdnqIrAj|*nag!VfS}$` zd!BugLP6!C;mG56&abD}3iT?XlM?OjRdK`CHgwORq`MnqLdNgYoa!_ni+{kBa}lza zn(=D!0FNp=RHcT)cQhfIm$ zpC8_ITuDv-x6}h@nDciO*{c!fdJPnV{~?2mGm;wj28-VRRdJ*YZpD~kW9zsrT*