diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..69d4730 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,74 @@ +## 2.2.1 [b5e48a0](https://github.com/sipeed/NanoKVM/commit/b5e48a07e82df3aedd60442342ae50b95684a697) (2025-02-21) + +* fix: mounted image were not being detected correctly +* perf: add support for CD-ROM mode when mounting image (thanks to [@scpcom](https://github.com/scpcom)) +* perf: add a loading state during login +* perf: add changelog link in settings +* perf: update translation and cleanup the code (thanks to [@ChokunPlayZ](https://github.com/ChokunPlayZ) [@Stoufiler](https://github.com/Stoufiler) [@polyzium](https://github.com/polyzium) [@Jonher937](https://github.com/Jonher937) [@S33G](https://github.com/S33G)) + +## 2.2.0 [0dbf8c0](https://github.com/sipeed/NanoKVM/commit/0dbf8c007f2d0183d0f0601c3da6d3c3fccd8b31) (2025-02-17) + +NanoKVM [Image v1.4.0](https://github.com/sipeed/NanoKVM/releases/tag/v1.4.0) has been released! + +> Please refer to the [wiki](https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/system/introduction.html) for more details about the image and application. + +* fix: improve password update notification logic (thanks to [@li20034](https://github.com/li20034)) +* perf: increase update wait time to 10s (from 6s) +* perf: update Korean translation (thanks to [@forumi0721](https://github.com/forumi0721)) +* perf: update Traditional Chinese translation (thanks to [@protonchang](https://github.com/protonchang)) +* refactor: update `libkvm.so` and `libkvm_mmf.so` libraries + +## 2.1.6 [6eb4a4e](https://github.com/sipeed/NanoKVM/commit/6eb4a4ea6254f465a47f9881d13934c686649061) (2025-02-14) + +* feat: support downloading image from online URL (thanks to [@Itxaka](https://github.com/Itxaka)) +* feat: add keyboard shortcut `Ctrl+Alt+Del` (thanks to [@CaffeeLake](https://github.com/CaffeeLake)) +* fix: fix the CSRF issue +* perf: add an option to configure custom ICE servers (thanks to [@VMFortress](https://github.com/VMFortress)) +* perf: removed unnecessary modifications to DNS configuration +* perf: add an SSH enable/disable toggle in the web UI +* perf: add a Tailscale enable/disable toggle in the web UI +* perf: download Tailscale installation package from the official source +* perf: automatic enable/disable GOMEMLIMIT on tailscale start/stop +* perf: add JWT configuration + * secretKey: customize secret key. If empty, generated a random 64-byte secret key by default + * refreshTokenDuration: customize token expiration time + * revokeTokensOnLogout: invalidate all JWT tokens on logout +* perf: implement secure password storage using bcrypt hashing +* perf: implement integrity checks for online updates +* refactor: refactor HDMI module and remove the dependency `libmaixcam_lib.so` +* refactor: web terminal use pty instead of SSH +* refactor: move Tailscale APIs from the `network` module to the `extensions` module + +## 2.1.5 [85f6447](https://github.com/sipeed/NanoKVM/commit/85f6447a16cc2591c6459b7d3dfda4d4cb75e98c) (2025-01-14) + +* feat: add HDMI reset for NanoKVM-PCIe +* fix: remove unnecessary lock acquisition during HID reset +* refactor: refactor Tailscale + +## 2.1.4 [d7ca7c4](https://github.com/sipeed/NanoKVM/commit/d7ca7c453d821ad099bf79b463969419041279cb) (2025-01-10) + +* feat: support configuring OLED sleep settings +* feat: support setting the `GOMEMLIMIT` environment variable +* fix: fix Wi-Fi configuration +* perf: password changes now update both the web user and the system root user +* perf: add MAC address verification for Wake-on-LAN +* refactor: a lot update to web UI +* refactor: refactor Tailscale + +## 2.1.3 [26078fe](https://github.com/sipeed/NanoKVM/commit/26078fe46e43d4543d7b09901b4992e4fbe4f01f) (2024-12-27) + +* feat: add API to retrieve Wi-Fi information +* fix: fix keyboard modifier keys +* fix: update keyboard and mouse HID codes +* fix: update hardware version information + +## 2.1.2 [5a39562](https://github.com/sipeed/NanoKVM/commit/5a39562f2d32695933f4e7e86866136236cc9903) (2024-12-04) + +* feat: add hardware version to configuration +* feat: add Wi-Fi configuration support for NanoKVM-PCIe +* perf: update web UI +* chore: add dependency libraries + +## 2.1.1 [74a303b](https://github.com/sipeed/NanoKVM/commit/74a303bd5cbb58f9d8ddd81abaaf4919dbbfb71b) (2024-11-06) + +* feat: support h264 diff --git a/README.md b/README.md index 7b2c8b9..45802c2 100644 --- a/README.md +++ b/README.md @@ -15,19 +15,25 @@ English | [中文](./README_ZH.md) | [日本語](./README_JA.md) -> Your NanoKVM Power by RISC-V ! +> Your NanoKVM is powered by [RISC-V](https://en.wikipedia.org/wiki/RISC-V)! > If you have any issues or suggestions, creating issue here, or tell us with [MaixHub Discussion](https://maixhub.com/discussion/nanokvm). ## Opensource & Contribution +### Update 2025.02.19 +All components of NanoKVM were open-sourced, including the [front-end](https://github.com/sipeed/NanoKVM/tree/main/web), [back-end](https://github.com/sipeed/NanoKVM/tree/main/server), [kvm_vision](https://github.com/sipeed/NanoKVM/tree/main/vision/components/kvm), [kvm_mmf](https://github.com/sipeed/NanoKVM/tree/main/vision/components/kvm_mmf), [kvm_system](https://github.com/sipeed/NanoKVM/tree/main/support), the [kvmapp update package](https://github.com/sipeed/NanoKVM-System/tree/main/kvmapp), [system sdk](https://github.com/sipeed/LicheeRV-Nano-Build/tree/NanoKVM), and [packaging methods](https://github.com/sipeed/LicheeRV-Nano-Build/blob/NanoKVM/kvm/NanoKVM_img.sh). + +### Update 2025.02.14 +Newest APP v2.1.6 add most security enhancements&bug fix, recommend regular users upgrade to this version or newer. + ### Update 2025.02.05 For users concerned about security, you can read this issue: https://github.com/sipeed/NanoKVM/issues/301 , you will find explanations addressing all security concerns. GitHub, as an open and transparent platform, provides a space for discussing so-called "backdoor" concerns. Open-source makes products more secure! ### Update 2024.10.18 -We have opensourced the backend code in 10.8, and thank you for Civil PR it immediately, we will give you newest NanoKVM-PCIe as a reward! -The mid-Oct batch is starting testing and packaging, most order before 10.1 will ship out next and next next week. +We have open-sourced the backend code in 10.8, and thank you for civil PR it immediately, we will give you newest NanoKVM-PCIe as a reward! +The mid-Oct batch is starting testing and packaging, most orders before 10.1 will ship out next and next next week. The aliexpress shipping date is change to Dec since 10.1, it is a conservative delivery time. the order in Oct. should send out during 11.15~12.15. ## Introduction @@ -37,33 +43,33 @@ The Lichee NanoKVM is available in two versions: NanoKVM Lite is a basic configuration that is suitable for individual users with certain DIY capabilities and enterprise users with bulk requirements. The NanoKVM Full is a full version with a sophisticated case and complete accessories, as well as a built-in system mirror card that is ready to use at boot, and is recommended for individual users. -![](https://wiki.sipeed.com/hardware/zh/kvm/assets/NanoKVM/1_intro/NanoKVM_1.jpg) +![NanoKVM Disassembled](https://wiki.sipeed.com/hardware/zh/kvm/assets/NanoKVM/1_intro/NanoKVM_1.jpg) ## Tech Specs -| Products | NanoKVM (Lite) | NanoKVM (Full) | PiKVM V4 | -| --- | --- | --- | --- | -| Compute Units | LicheeRV Nano(RISCV) | LicheeRV Nano(RISCV) | CM4 (ARM) | -| Resolution | 1080P @ 60fps | 1080P @ 60fps | 1080P @ 60fps | -| Video encoding | MJPEG, H264(developing) | MJPEG, H264(developing) | MJPEG, H264 | -| Video delay | 90~230ms | 90~230ms | 100~230ms | -| UEFI/BIOS | ✓ | ✓ | ✓ | -| Virtual HID | ✓ | ✓ | ✓ | -| Virtual CD-ROM | ✓ | ✓ | ✓ | -| IPMI | ✓ | ✓ | ✓ | -| Wake-on-LAN | ✓ | ✓ | ✓ | -| ETH | 100M/10M | 100M/10M | 1000M/100M/10M | -| ATX power control | None,Users can connect by their own | USB interface IO control board | RJ-45 interface IO control board | -| OLED | None,Users can connect by their own | 128x64 0.96" white | 128x32 0.91" white | -| UART | 2 | 2 | 1 | -| TF Card | None | ✓ | ✓ | -| Expansion | None | PoE | WiFi/LTE | -| Power consumption | 0.2A@5V | 0.2A@5V | Peak 2.6A@5V | -| Power input | PC USB can be powered | PC USB or auxiliary power supply | DC 5V 3A power supply is required | -| Cooling | Silent fanless | Silent fanless | Fan cooling | -| Size | 23x37x15mm
~1/30 PiKVM V4 size | 40x36x36mm
~1/7 PiKVM V4 size | 120x68x44mm | +| Products | NanoKVM (Lite) | NanoKVM (Full) | PiKVM V4 | +|------------------- |-------------------------------------- |---------------------------------- |----------------------------------- | +| Compute Units | LicheeRV Nano(RISCV) | LicheeRV Nano(RISCV) | CM4 (ARM) | +| Resolution | 1080P @ 60fps | 1080P @ 60fps | 1080P @ 60fps | +| Video encoding | MJPEG, H264 | MJPEG, H264 | MJPEG, H264 | +| Video delay | 90~230ms | 90~230ms | 100~230ms | +| UEFI/BIOS | ✓ | ✓ | ✓ | +| Virtual HID | ✓ | ✓ | ✓ | +| Virtual CD-ROM | ✓ | ✓ | ✓ | +| IPMI | ✓ | ✓ | ✓ | +| Wake-on-LAN | ✓ | ✓ | ✓ | +| ETH | 100M/10M | 100M/10M | 1000M/100M/10M | +| ATX power control | None,Users can connect by their own | USB interface IO control board | RJ-45 interface IO control board | +| OLED | None,Users can connect by their own | 128x64 0.96" white | 128x32 0.91" white | +| UART | 2 | 2 | 1 | +| TF Card | None | ✓ | ✓ | +| Expansion | None | PoE | WiFi/LTE | +| Power consumption | 0.2A@5V | 0.2A@5V | Peak 2.6A@5V | +| Power input | PC USB can be powered | PC USB or auxiliary power supply | DC 5V 3A power supply is required | +| Cooling | Silent fanless | Silent fanless | Fan cooling | +| Size | 23x37x15mm ~1/30 PiKVM V4 size | 40x36x36mm ~1/7 PiKVM V4 size | 120x68x44mm | -![](https://wiki.sipeed.com/hardware/zh/kvm/assets/NanoKVM/1_intro/NanoKVM_2.jpg) +![NanoKVM PCB Pinout](https://wiki.sipeed.com/hardware/zh/kvm/assets/NanoKVM/1_intro/NanoKVM_2.jpg) ## Hardware platform NanoKVM @@ -73,7 +79,7 @@ The NanoKVM Lite is constructed by the LicheeRV Nano and HDMItoCSI board, and th The NanoKVM image is built on LicheeRV Nano SDK and MaixCDK, and is compatible with materials that use the LicheeRV Nano, opposite the KVM software cannot be used with the LicheeRV Nano or other SG2002 products. If you would like to build an HDMI input application on LicheeRV Nano or MaixCam, please contact us for technical support. -Note: Out of the 256MB memory in SG2002, 158MB is currently allocated for the multimedia subsystem, which NanoKVM will use for video image acquisition and processing +Note: Out of the 256MB memory in SG2002, 158MB is currently allocated for the multimedia subsystem, which NanoKVM will use for video image acquisition and processing. + [NanoKVM-A Schematic](https://cn.dl.sipeed.com/fileList/KVM/nanoKVM/HDK/02_Schematic/SCH_RV_Nano_KVM_A_30111.pdf) + [NanoKVM-B Schematic](https://cn.dl.sipeed.com/fileList/KVM/nanoKVM/HDK/02_Schematic/SCH_HDMI_MIPI_31011.pdf) @@ -85,21 +91,28 @@ Note: Out of the 256MB memory in SG2002, 158MB is currently allocated for the mu **Enhancements:** -- [ ] Refactor the HDMI module and remove the libmaixcam_lib.so dependency -- [ ] Disable SSH by default and add an enable/disable toggle in the web UI -- [ ] Disable Tailscale by default and add an enable/disable toggle in the web UI -- [ ] Add JWT configuration options to the configuration file +- [x] Refactor the HDMI module and remove the libmaixcam_lib.so dependency +- [x] Disable SSH by default and add an enable/disable toggle in the web UI +- [x] Disable Tailscale by default and add an enable/disable toggle in the web UI +- [x] Add JWT configuration options to the configuration file +- [x] Secure password storage with bcrypt **Distribution:** -- [ ] Distribute applications via GitHub -- [ ] Implement integrity checks for online updates +- [x] Distribute applications via GitHub +- [x] Implement integrity checks for online updates - [ ] Support offline updates **Bug Fixes:** -- [ ] Fix the DNS issue -- [ ] Fix the CSRF vulnerability +- [x] Fix the DNS issue +- [x] Fix the CSRF vulnerability + +**Features** + +- [ ] [79](https://github.com/sipeed/NanoKVM/issues/79) Support Zerotier +- [ ] [99](https://github.com/sipeed/NanoKVM/issues/99) Support WireGuard +- [ ] [249](https://github.com/sipeed/NanoKVM/issues/249) Add Mouse Jiggler ## Where to buy @@ -107,11 +120,7 @@ Note: Out of the 256MB memory in SG2002, 158MB is currently allocated for the mu * [淘宝](https://item.taobao.com/item.htm?id=811206560480) * [Preorder (anyother country that not support in Aliexpress or Taobao)](https://sipeed.com/nanokvm) - ## Community * [MaixHub Discussion](https://maixhub.com/discussion/nanokvm) * QQ group: 703230713 - - - diff --git a/README_JA.md b/README_JA.md index 2555f89..1bdb4a4 100644 --- a/README_JA.md +++ b/README_JA.md @@ -41,7 +41,7 @@ NanoKVM Fullは完全版で、洗練されたケースと完全なアクセサ | --- | --- | --- | --- | | 計算ユニット | LicheeRV Nano(RISCV) | LicheeRV Nano(RISCV) | CM4 (ARM) | | 解像度 | 1080P @ 60fps | 1080P @ 60fps | 1080P @ 60fps | -| ビデオエンコーディング | MJPEG, H264(開発中) | MJPEG, H264(開発中) | MJPEG, H264 | +| ビデオエンコーディング | MJPEG, H264 | MJPEG, H264 | MJPEG, H264 | | ビデオ遅延 | 90~230ms | 90~230ms | 100~230ms | | UEFI/BIOS | ✓ | ✓ | ✓ | | 仮想HID | ✓ | ✓ | ✓ | diff --git a/README_ZH.md b/README_ZH.md index 4012b82..a4f0730 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -33,7 +33,7 @@ NanoKVM Full 为完整版配置,带精致外壳和完整配件,内置开机 | --- | --- | --- | --- | | 计算单元 | LicheeRV Nano(RISCV) | LicheeRV Nano(RISCV) | CM4 (ARM) | | 分辨率 | 1080P @ 60fps | 1080P @ 60fps | 1080P @ 60fps | -| 视频编码 | MJPEG, H264(developing) | MJPEG, H264(developing) | MJPEG, H264 | +| 视频编码 | MJPEG, H264 | MJPEG, H264 | MJPEG, H264 | | 视频延迟 | 90~230ms | 90~230ms | 100~230ms | | UEFI/BIOS | ✓ | ✓ | ✓ | | 模拟USB键鼠 | ✓ | ✓ | ✓ | diff --git a/server/README.md b/server/README.md index 831bfdb..0dc544e 100644 --- a/server/README.md +++ b/server/README.md @@ -44,9 +44,13 @@ logger: # Note: Only disable authentication in development environment authentication: enable -# JWT secret key configuration -# If left empty, a random key will be generated on each server start -secretKey: "" +jwt: + # JWT secret key. If left empty, a random 64-byte key will be generated automatically. + secretKey: "" + # JWT token expiration time in seconds. Default: 2678400 (31 days) + refreshTokenDuration: 2678400 + # Invalidate all JWT tokens when the user logs out. Default: true + revokeTokensOnLogout: true # Address for custom STUN server stun: stun.l.google.com:19302 @@ -80,6 +84,15 @@ Note: Use Linux operating system (x86-64). This build process is not compatible 3. Run `patchelf --add-rpath \$ORIGIN/dl_lib NanoKVM-Server` to modify the RPATH of the executable file. 4. Deploy the Application - 1. Before deploying, update the application to the latest version in your browser. Follow the instructions [here](https://wiki.sipeed.com/hardware/en/kvm/NanoKVM/system/updating.html). + 1. File uploads requires SSH. Please enable it in the Web Settings: `Settings > SSH`; 2. Replace the original file in the NanoKVM `/kvmapp/server/` directory with the newly compiled `NanoKVM-Server`. - 3. Restart the service on NanoKVM by executing `/etc/init.d/S95nanokvm restart`. \ No newline at end of file + 3. Restart the service on NanoKVM by executing `/etc/init.d/S95nanokvm restart`. + +## Manually Update + +> File uploads requires SSH. Please enable it in the Web Settings: `Settings > SSH`; + +1. Download the latest application from [GitHub](https://github.com/sipeed/NanoKVM/releases); +2. Unzip the downloaded file and rename the unzipped folder to `kvmapp`; +3. Back up the existing `/kvmapp` directory on your NanoKVM, then replace it with the new `kvmapp` folder; +4. Run `/etc/init.d/S95nanokvm restart` on your NanoKVM to restart the service. diff --git a/server/README_ZH.md b/server/README_ZH.md index 1050fa2..acfe6dd 100644 --- a/server/README_ZH.md +++ b/server/README_ZH.md @@ -42,9 +42,13 @@ logger: # 注意:生产环境中请勿使用 disable。 authentication: enable -# JWT 密钥 -# 如果不设置,则每次服务启动时使用随机生成的密钥。 -secretKey: "" +jwt: + # jwt 密钥。设置为空则使用随机生成的64位密钥 + secretKey: "" + # jwt token 过期时间(单位:秒),默认为2678400(31天) + refreshTokenDuration: 2678400 + # 在帐号登出时是否使所有 jwt token 失效。默认为 true + revokeTokensOnLogout: true stun: stun.l.google.com:19302 @@ -75,6 +79,15 @@ turn: 3. 执行 `patchelf --add-rpath \$ORIGIN/dl_lib NanoKVM-Server` 修改可执行文件的 RPATH 属性。 4. 部署 - 1. 部署前,请先在浏览器中将应用[更新](https://wiki.sipeed.com/hardware/zh/kvm/NanoKVM/system/updating.html)到最新版本; - 2. 使用编译生成的 `NanoKVM-Server` 文件,替换 NanoKVM 中 `/kvmapp/server/` 目录下的原始文件; + 1. 上传文件需要启用 SSH 功能。请在 Web `设置 - SSH` 中检查 SSH 是否已经启用; + 2. 使用编译生成的 `NanoKVM-Server` 文件,替换 NanoKVM 中 `/kvmapp/server/` 目录下的原始文件; 3. 在 NanoKVM 中执行 `/etc/init.d/S95nanokvm restart` 重启服务。 + +## 手动更新 + +> 请确保已经在 Web 界面的 `设置 - SSH` 中启用了 SSH 功能,以便上传文件。 + +1. 从 [GitHub](https://github.com/sipeed/NanoKVM/releases) 下载最新的应用安装包; +2. 解压缩下载的安装包,并将解压后的文件夹重命名为 `kvmapp`; +3. 备份 NanoKVM 系统中的 `/kvmapp` 目录,然后用解压后的 `kvmapp` 文件夹替换现有目录。 +4. 在 NanoKVM 中执行 `/etc/init.d/S95nanokvm restart` 重启服务。 \ No newline at end of file diff --git a/server/common/cgo.go b/server/common/cgo.go index f618eee..6289d30 100644 --- a/server/common/cgo.go +++ b/server/common/cgo.go @@ -139,7 +139,7 @@ func (k *KvmVision) ReadH264PPS() ([]byte, int) { return data, result } -func (k *KvmVision) EnableHdmi(enable bool) int { +func (k *KvmVision) SetHDMI(enable bool) int { hdmiEnable := C.uint8_t(0) if enable { hdmiEnable = C.uint8_t(1) diff --git a/server/config/config.go b/server/config/config.go index f1367a8..ab982b7 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -2,28 +2,24 @@ package config import ( "bytes" - "crypto/rand" - "encoding/base64" "errors" - "fmt" "log" "os" "sync" - "time" "github.com/spf13/viper" "gopkg.in/yaml.v3" ) var ( - config Config - once sync.Once + instance Config + once sync.Once ) func GetInstance() *Config { once.Do(initialize) - return &config + return &instance } func initialize() { @@ -43,20 +39,16 @@ func initialize() { log.Fatalf("Failed to validate configuration!") } - if err := viper.Unmarshal(&config); err != nil { + if err := viper.Unmarshal(&instance); err != nil { log.Fatalf("Failed to parse configuration: %s", err) } - if config.Authentication == "disable" { + checkDefaultValue() + + if instance.Authentication == "disable" { log.Println("NOTICE: Authentication is disabled! Please ensure your service is secure!") } - if config.SecretKey == "" { - config.SecretKey = generateRandomString() - } - - config.Hardware = getHardware() - log.Println("config loaded successfully") } @@ -128,16 +120,3 @@ func validate() error { return readByDefault() } - -// Generate random string for secret key. -func generateRandomString() string { - b := make([]byte, 64) - _, err := rand.Read(b) - if err != nil { - currentTime := time.Now().UnixNano() - timeString := fmt.Sprintf("%d", currentTime) - return fmt.Sprintf("%064s", timeString) - } - - return base64.URLEncoding.EncodeToString(b) -} diff --git a/server/config/default.go b/server/config/default.go index 9ec3075..d58ebe5 100644 --- a/server/config/default.go +++ b/server/config/default.go @@ -14,6 +14,11 @@ var defaultConfig = &Config{ Level: "info", File: "stdout", }, + JWT: JWT{ + SecretKey: "", + RefreshTokenDuration: 2678400, + RevokeTokensOnLogout: true, + }, Stun: "stun.l.google.com:19302", Turn: Turn{ TurnAddr: "turn.cloudflare.com:3478", @@ -22,3 +27,24 @@ var defaultConfig = &Config{ }, Authentication: "enable", } + +func checkDefaultValue() { + if instance.JWT.SecretKey == "" { + instance.JWT.SecretKey = generateRandomSecretKey() + instance.JWT.RevokeTokensOnLogout = true + } + + if instance.JWT.RefreshTokenDuration == 0 { + instance.JWT.RefreshTokenDuration = 2678400 + } + + if instance.Stun == "" { + instance.Stun = "stun.l.google.com:19302" + } + + if instance.Authentication == "" { + instance.Authentication = "enable" + } + + instance.Hardware = getHardware() +} diff --git a/server/config/hardware.go b/server/config/hardware.go index 981324f..79d4e2f 100644 --- a/server/config/hardware.go +++ b/server/config/hardware.go @@ -85,7 +85,7 @@ func getHardware() (h Hardware) { default: h = HWAlpha - log.Error("Unsupported hardware version: %s", version) + log.Errorf("Unsupported hardware version: %s", version) } return diff --git a/server/config/jwt.go b/server/config/jwt.go new file mode 100644 index 0000000..9edd85b --- /dev/null +++ b/server/config/jwt.go @@ -0,0 +1,28 @@ +package config + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "time" +) + +// RegenerateSecretKey regenerate secret key when logout +func RegenerateSecretKey() { + if instance.JWT.RevokeTokensOnLogout { + instance.JWT.SecretKey = generateRandomSecretKey() + } +} + +// Generate random string for secret key. +func generateRandomSecretKey() string { + b := make([]byte, 64) + _, err := rand.Read(b) + if err != nil { + currentTime := time.Now().UnixNano() + timeString := fmt.Sprintf("%d", currentTime) + return fmt.Sprintf("%064s", timeString) + } + + return base64.URLEncoding.EncodeToString(b) +} diff --git a/server/config/types.go b/server/config/types.go index 3ad492e..2f8f069 100644 --- a/server/config/types.go +++ b/server/config/types.go @@ -6,7 +6,7 @@ type Config struct { Cert Cert `yaml:"cert"` Logger Logger `yaml:"logger"` Authentication string `yaml:"authentication"` - SecretKey string `yaml:"secretKey"` + JWT JWT `yaml:"jwt"` Stun string `yaml:"stun"` Turn Turn `yaml:"turn"` @@ -28,6 +28,12 @@ type Cert struct { Key string `yaml:"key"` } +type JWT struct { + SecretKey string `yaml:"secretKey"` + RefreshTokenDuration uint64 `yaml:"refreshTokenDuration"` + RevokeTokensOnLogout bool `yaml:"revokeTokensOnLogout"` +} + type Turn struct { TurnAddr string `yaml:"turnAddr"` TurnUser string `yaml:"turnUser"` diff --git a/server/dl_lib/libkvm.so b/server/dl_lib/libkvm.so index 68045f9..a79e1d9 100644 Binary files a/server/dl_lib/libkvm.so and b/server/dl_lib/libkvm.so differ diff --git a/server/dl_lib/libkvm_mmf.so b/server/dl_lib/libkvm_mmf.so new file mode 100644 index 0000000..992c7ab Binary files /dev/null and b/server/dl_lib/libkvm_mmf.so differ diff --git a/server/dl_lib/libmaixcam_lib.so b/server/dl_lib/libmaixcam_lib.so deleted file mode 100644 index fa47a05..0000000 Binary files a/server/dl_lib/libmaixcam_lib.so and /dev/null differ diff --git a/server/go.mod b/server/go.mod index 7637c82..b123016 100644 --- a/server/go.mod +++ b/server/go.mod @@ -3,6 +3,7 @@ module NanoKVM-Server go 1.22.1 require ( + github.com/creack/pty v1.1.24 github.com/gin-gonic/contrib v0.0.0-20240508051311-c1c6bf0061b0 github.com/gin-gonic/gin v1.10.0 github.com/go-playground/validator/v10 v10.20.0 diff --git a/server/go.sum b/server/go.sum index 9f44074..271cc14 100644 --- a/server/go.sum +++ b/server/go.sum @@ -6,6 +6,8 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -156,28 +158,17 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= diff --git a/server/include/kvm_vision.h b/server/include/kvm_vision.h index 2fa2a25..f4b013c 100644 --- a/server/include/kvm_vision.h +++ b/server/include/kvm_vision.h @@ -37,6 +37,7 @@ void kvmv_init(uint8_t _debug_info_en); * @param _pp_kvm_data @output: Encode data * @param _p_kvmv_data_size @output: Encode data size * @return + -4: Modifying image resolution, please wait -3: img buffer full -2: VENC Error -1: No images were acquired diff --git a/server/main.go b/server/main.go index 4dbdee7..8db3e77 100644 --- a/server/main.go +++ b/server/main.go @@ -8,9 +8,11 @@ import ( "NanoKVM-Server/router" "NanoKVM-Server/utils" "fmt" + "log" "os" "os/signal" "syscall" + "time" "github.com/gin-gonic/gin" cors "github.com/rs/cors/wrapper/gin" @@ -19,33 +21,51 @@ import ( func main() { initialize() defer dispose() - signalHandler() - gin.SetMode(gin.ReleaseMode) - r := gin.New() - r.Use(gin.Recovery()) - r.Use(cors.AllowAll()) - - router.Init(r) - - run(r) + run() } func initialize() { logger.Init() + // init screen parameters _ = common.GetScreen() - _ = common.GetKvmVision() + + // init HDMI + vision := common.GetKvmVision() + vision.SetHDMI(false) + time.Sleep(10 * time.Millisecond) + vision.SetHDMI(true) utils.InitGoMemLimit() + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + go func() { + sig := <-sigChan + log.Printf("\nReceived signal: %v\n", sig) + + dispose() + os.Exit(0) + }() } -func run(r *gin.Engine) { +func run() { conf := config.GetInstance() + gin.SetMode(gin.ReleaseMode) + r := gin.New() + r.Use(gin.Recovery()) + + if conf.Authentication == "disable" { + r.Use(cors.AllowAll()) + } + + router.Init(r) + httpAddr := fmt.Sprintf(":%d", conf.Port.Http) httpsAddr := fmt.Sprintf(":%d", conf.Port.Https) - fmt.Printf("proto: %s, port: %d %d\n", conf.Proto, conf.Port.Http, conf.Port.Https) + log.Printf("proto: %s, port: %d %d\n", conf.Proto, conf.Port.Http, conf.Port.Https) if conf.Proto == "https" { r.Use(middleware.Tls()) @@ -69,16 +89,3 @@ func run(r *gin.Engine) { func dispose() { common.GetKvmVision().Close() } - -func signalHandler() { - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) - - go func() { - sig := <-sigChan - fmt.Printf("\nReceived signal: %v\n", sig) - - dispose() - os.Exit(0) - }() -} diff --git a/server/middleware/jwt.go b/server/middleware/jwt.go index 61d7616..8602881 100644 --- a/server/middleware/jwt.go +++ b/server/middleware/jwt.go @@ -16,10 +16,6 @@ type Token struct { jwt.RegisteredClaims } -const ( - ExpireDuration = 31 * 24 * time.Hour -) - func CheckToken() gin.HandlerFunc { return func(c *gin.Context) { conf := config.GetInstance() @@ -46,24 +42,25 @@ func CheckToken() gin.HandlerFunc { func GenerateJWT(username string) (string, error) { conf := config.GetInstance() + expireDuration := time.Duration(conf.JWT.RefreshTokenDuration) * time.Second + claims := Token{ Username: username, RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(time.Now().Add(ExpireDuration)), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(expireDuration)), }, } t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return t.SignedString([]byte(conf.SecretKey)) + return t.SignedString([]byte(conf.JWT.SecretKey)) } func ParseJWT(jwtToken string) (*Token, error) { conf := config.GetInstance() t, err := jwt.ParseWithClaims(jwtToken, &Token{}, func(token *jwt.Token) (interface{}, error) { - secretKey := conf.SecretKey - return []byte(secretKey), nil + return []byte(conf.JWT.SecretKey), nil }) if err != nil { log.Debugf("parse jwt error: %s", err) diff --git a/server/proto/download.go b/server/proto/download.go index fa122ae..461fb9f 100644 --- a/server/proto/download.go +++ b/server/proto/download.go @@ -1,7 +1,7 @@ package proto type ImageEnabledRsp struct { - Enabled bool `json:"enabled"` + Enabled bool `json:"enabled"` } type StatusImageRsp struct { diff --git a/server/proto/storage.go b/server/proto/storage.go index 75a6124..33c25cf 100644 --- a/server/proto/storage.go +++ b/server/proto/storage.go @@ -5,9 +5,14 @@ type GetImagesRsp struct { } type MountImageReq struct { - File string `json:"file" validate:"omitempty"` + File string `json:"file" validate:"omitempty"` + Cdrom bool `json:"cdrom" validate:"omitempty"` } type GetMountedImageRsp struct { File string `json:"file"` } + +type GetCdRomRsp struct { + Cdrom int64 `json:"cdrom"` +} diff --git a/server/proto/vm.go b/server/proto/vm.go index 75c7a9c..7529a8e 100644 --- a/server/proto/vm.go +++ b/server/proto/vm.go @@ -79,3 +79,7 @@ type GetOLEDRsp struct { Exist bool `json:"exist"` Sleep int `json:"sleep"` } + +type GetSSHStateRsp struct { + Enabled bool `json:"enabled"` +} diff --git a/server/router/auth.go b/server/router/auth.go index 5115a92..aa237fa 100644 --- a/server/router/auth.go +++ b/server/router/auth.go @@ -17,4 +17,5 @@ func authRouter(r *gin.Engine) { api.GET("/auth/password", service.IsPasswordUpdated) // is password updated api.GET("/auth/account", service.GetAccount) // get account api.POST("/auth/password", service.ChangePassword) // change password + api.POST("/auth/logout", service.Logout) // logout } diff --git a/server/router/extensions.go b/server/router/extensions.go new file mode 100644 index 0000000..696a94d --- /dev/null +++ b/server/router/extensions.go @@ -0,0 +1,25 @@ +package router + +import ( + "NanoKVM-Server/middleware" + "NanoKVM-Server/service/extensions/tailscale" + + "github.com/gin-gonic/gin" +) + +func extensionsRouter(r *gin.Engine) { + api := r.Group("/api/extensions").Use(middleware.CheckToken()) + + ts := tailscale.NewService() + + api.POST("/tailscale/install", ts.Install) // install tailscale + api.POST("/tailscale/uninstall", ts.Uninstall) // uninstall tailscale + api.GET("/tailscale/status", ts.GetStatus) // get tailscale status + api.POST("/tailscale/up", ts.Up) // run tailscale up + api.POST("/tailscale/down", ts.Down) // run tailscale down + api.POST("/tailscale/login", ts.Login) // tailscale login + api.POST("/tailscale/logout", ts.Logout) // tailscale logout + api.POST("/tailscale/start", ts.Start) // tailscale start + api.POST("/tailscale/stop", ts.Stop) // tailscale stop + api.POST("/tailscale/restart", ts.Restart) // tailscale restart +} diff --git a/server/router/network.go b/server/router/network.go index 906c2bb..08e4a6f 100644 --- a/server/router/network.go +++ b/server/router/network.go @@ -18,15 +18,5 @@ func networkRouter(r *gin.Engine) { api.GET("/network/wol/mac", service.GetMac) // get mac list api.DELETE("/network/wol/mac", service.DeleteMac) // delete mac - api.POST("/network/tailscale/install", service.TsInstall) // install tailscale - api.POST("/network/tailscale/uninstall", service.TsUninstall) // uninstall tailscale - api.GET("/network/tailscale/status", service.GetTsStatus) // get tailscale status - api.POST("/network/tailscale/up", service.TsUp) // run tailscale up - api.POST("/network/tailscale/down", service.TsDown) // run tailscale down - api.POST("/network/tailscale/login", service.TsLogin) // tailscale login - api.POST("/network/tailscale/logout", service.TsLogout) // tailscale logout - api.POST("/network/tailscale/stop", service.TsStop) // tailscale stop - api.POST("/network/tailscale/restart", service.TsRestart) // tailscale restart - api.GET("/network/wifi", service.GetWifi) // get Wi-Fi information } diff --git a/server/router/router.go b/server/router/router.go index 2c0e918..8b6148a 100644 --- a/server/router/router.go +++ b/server/router/router.go @@ -38,4 +38,5 @@ func server(r *gin.Engine) { hidRouter(r) wsRouter(r) downloadRouter(r) + extensionsRouter(r) } diff --git a/server/router/storage.go b/server/router/storage.go index f7fdf89..d29a327 100644 --- a/server/router/storage.go +++ b/server/router/storage.go @@ -14,4 +14,5 @@ func storageRouter(r *gin.Engine) { api.GET("/storage/image", service.GetImages) // get image list api.GET("/storage/image/mounted", service.GetMountedImage) // get mounted image api.POST("/storage/image/mount", service.MountImage) // mount image + api.GET("/storage/cdrom", service.GetCdRom) // get CD-ROM flag } diff --git a/server/router/vm.go b/server/router/vm.go index d8bcb94..1dced36 100644 --- a/server/router/vm.go +++ b/server/router/vm.go @@ -35,4 +35,8 @@ func vmRouter(r *gin.Engine) { api.POST("/vm/oled", service.SetOLED) // set OLED configuration api.POST("/vm/hdmi/reset", service.ResetHdmi) // reset hdmi (pcie only) + + api.GET("/vm/ssh", service.GetSSHState) // get SSH state + api.POST("/vm/ssh/enable", service.EnableSSH) // enable SSH + api.POST("/vm/ssh/disable", service.DisableSSH) // disable SSH } diff --git a/server/service/application/application.go b/server/service/application/application.go deleted file mode 100644 index 445dd20..0000000 --- a/server/service/application/application.go +++ /dev/null @@ -1,211 +0,0 @@ -package application - -import ( - "fmt" - "io" - "net/http" - "os" - "os/exec" - "strings" - "time" - - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" - - "NanoKVM-Server/proto" - "NanoKVM-Server/utils" -) - -const ( - maxTries = 3 -) - -func (s *Service) GetVersion(c *gin.Context) { - var rsp proto.Response - log.Debugf("get version api triggered") - - // current version - currentVersion := "1.0.0" - content, err := os.ReadFile(versionFile) - if err == nil { - currentVersion = strings.ReplaceAll(string(content), "\n", "") - } - log.Debugf("current version: %s", currentVersion) - - // latest version - url := fmt.Sprintf("%s?now=%d", versionURL, time.Now().Unix()) - resp, err := http.Get(url) - if err != nil { - log.Debugf("get latest version failed: %v", err) - rsp.ErrRsp(c, -2, "Unable to access sipeed.com. Please check your network.") - return - } - defer func() { - _ = resp.Body.Close() - }() - - body, err := io.ReadAll(resp.Body) - if err != nil { - log.Errorf("read body failed: %v", err) - rsp.ErrRsp(c, -4, "read body failed") - return - } - - if resp.StatusCode != http.StatusOK { - log.Errorf("server responded with status code: %d", resp.StatusCode) - rsp.ErrRsp(c, -3, fmt.Sprintf("get version failed, server returns status code: %d, body: %s", resp.StatusCode, string(body))) - return - } - - latestVersion := strings.ReplaceAll(string(body), "\n", "") - - rsp.OkRspWithData(c, &proto.GetVersionRsp{ - Current: currentVersion, - Latest: latestVersion, - }) -} - -func (s *Service) Update(c *gin.Context) { - var rsp proto.Response - log.Debugf("update application api triggered") - - if err := updateApp(); err != nil { - log.Debugf("update failed: %s", err) - rsp.ErrRsp(c, -1, fmt.Sprintf("update failed: %s", err)) - return - } - - rsp.OkRsp(c) - log.Debugf("update application success") - - // Sleep for a second before restarting the device - time.Sleep(1 * time.Second) - - _ = exec.Command("sh", "-c", "/etc/init.d/S95nanokvm restart").Run() -} - -func updateApp() error { - log.Debugf("update application") - _ = os.RemoveAll(temporary) - _ = os.MkdirAll(temporary, 0o755) - defer func() { - _ = os.RemoveAll(temporary) - }() - - if err := downloadLib(); err != nil { - log.Errorf("download lib failed: %s", err) - return err - } - - if err := downloadApp(); err != nil { - log.Errorf("download app failed: %s", err) - return err - } - - err := utils.MoveFile(temporary+"/"+libName, temporary+"/latest/kvm_system/dl_lib/"+libName) // move lib - if err != nil { - log.Errorf("rename lib failed: %s", err) - return err - } - - // backup old version - err = os.RemoveAll(backup) - if err != nil { - log.Errorf("remove backup failed: %s", err) - return err - } - - err = utils.MoveFilesRecursively(workspace, backup) - if err != nil { - log.Errorf("backuping old libraries failed: %s", err) - return err - } - - // update - err = utils.MoveFilesRecursively(temporary+"/latest", workspace) - if err != nil { - log.Errorf("failed to move update back in place: %s", err) - return err - } - - // modify permissions - err = utils.ChmodRecursively(workspace, 0o755) - if err != nil { - log.Errorf("chmod failed: %s", err) - return err - } - - return nil -} - -func downloadApp() error { - var err error - log.Debugf("downloading latest application...") - url := fmt.Sprintf("%s?now=%d", applicationURL, time.Now().Unix()) - for i := range maxTries { - log.Debugf("attempt #%d/%d", i+1, maxTries) - if i > 0 { - time.Sleep(time.Second * 3) // wait for 3 seconds before retrying the download attempt - } - - var req *http.Request - req, err = http.NewRequest("GET", url, nil) - if err != nil { - log.Errorf("new request err: %s", err) - continue - } - - zipFile := temporary + "/latest.zip" - log.Debugf("update will be saved to: %s", zipFile) - err = utils.Download(req, zipFile) - if err != nil { - log.Errorf("downloading latest application failed, try again...") - continue - } - - err = utils.Unzip(zipFile, temporary) - if err != nil { - log.Errorf("unzip app failed: %s", err) - continue - } - - return nil - } - return err -} - -func downloadLib() error { - log.Debugf("downloading libs...") - content, err := os.ReadFile("/device_key") - if err != nil { - log.Errorf("error reading device key: %s", err) - return err - } - deviceKey := strings.ReplaceAll(string(content), "\n", "") - - for i := range maxTries { - log.Debugf("attempt #%d/%d", i+1, maxTries) - if i > 0 { - time.Sleep(time.Second * 3) // wait for 3 seconds before retrying the download attempt - } - - var req *http.Request - url := fmt.Sprintf("%s?uid=%s", libURL, deviceKey) - req, err = http.NewRequest("GET", url, nil) - if err != nil { - log.Errorf("error creating new request: %s", err) - continue - } - req.Header.Set("token", "MaixVision2024") - - target := fmt.Sprintf("%s/%s", temporary, libName) - - err = utils.Download(req, target) - if err != nil { - log.Errorf("downloading lib failed: %s", err) - continue - } - return nil - } - return err -} diff --git a/server/service/application/service.go b/server/service/application/service.go index 62ba4f7..be29bd1 100644 --- a/server/service/application/service.go +++ b/server/service/application/service.go @@ -1,15 +1,11 @@ package application const ( - versionURL = "https://cdn.sipeed.com/nanokvm/latest" - applicationURL = "https://cdn.sipeed.com/nanokvm/latest.zip" - libURL = "https://maixvision.sipeed.com/api/v1/nanokvm/encryption" + BaseURL = "https://cdn.sipeed.com/nanokvm" - temporary = "/root/.kvmcache" - workspace = "/kvmapp" - backup = "/root/old" - libName = "libmaixcam_lib.so" - versionFile = "/kvmapp/version" + AppDir = "/kvmapp" + BackupDir = "/root/old" + CacheDir = "/root/.kvmcache" ) type Service struct{} diff --git a/server/service/application/update.go b/server/service/application/update.go new file mode 100644 index 0000000..6b53fc3 --- /dev/null +++ b/server/service/application/update.go @@ -0,0 +1,222 @@ +package application + +import ( + "crypto/sha512" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "strings" + "time" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + + "NanoKVM-Server/proto" + "NanoKVM-Server/utils" +) + +const ( + maxTries = 3 +) + +type Latest struct { + Version string `json:"version"` + Name string `json:"name"` + Sha512 string `json:"sha512"` + Size uint `json:"size"` +} + +func (s *Service) GetVersion(c *gin.Context) { + var rsp proto.Response + + // current version + currentVersion := "1.0.0" + + versionFile := fmt.Sprintf("%s/version", AppDir) + if version, err := os.ReadFile(versionFile); err == nil { + currentVersion = strings.ReplaceAll(string(version), "\n", "") + } + + log.Debugf("current version: %s", currentVersion) + + // latest version + latest, err := getLatest() + if err != nil { + rsp.ErrRsp(c, -1, "get latest version failed") + return + } + + rsp.OkRspWithData(c, &proto.GetVersionRsp{ + Current: currentVersion, + Latest: latest.Version, + }) +} + +func (s *Service) Update(c *gin.Context) { + var rsp proto.Response + + if err := update(); err != nil { + rsp.ErrRsp(c, -1, fmt.Sprintf("update failed: %s", err)) + return + } + + rsp.OkRsp(c) + log.Debugf("update application success") + + // Sleep for a second before restarting the device + time.Sleep(1 * time.Second) + + _ = exec.Command("sh", "-c", "/etc/init.d/S95nanokvm restart").Run() +} + +func update() error { + _ = os.RemoveAll(CacheDir) + _ = os.MkdirAll(CacheDir, 0o755) + defer func() { + _ = os.RemoveAll(CacheDir) + }() + + // get latest information + latest, err := getLatest() + if err != nil { + return err + } + + // download + url := fmt.Sprintf("%s/%s", BaseURL, latest.Name) + target := fmt.Sprintf("%s/%s", CacheDir, latest.Name) + + if err := download(url, target); err != nil { + log.Errorf("download app failed: %s", err) + return err + } + + // check sha512 + if err := checksum(target, latest.Sha512); err != nil { + log.Errorf("check sha512 failed: %s", err) + return err + } + + // decompress + dir, err := utils.UnTarGz(target, CacheDir) + log.Debugf("untar: %s", dir) + if err != nil { + log.Errorf("decompress app failed: %s", err) + return err + } + + // backup old version + if err := os.RemoveAll(BackupDir); err != nil { + log.Errorf("remove backup failed: %s", err) + return err + } + + if err := utils.MoveFilesRecursively(AppDir, BackupDir); err != nil { + log.Errorf("backup app failed: %s", err) + return err + } + + // update + if err := utils.MoveFilesRecursively(dir, AppDir); err != nil { + log.Errorf("failed to move update back in place: %s", err) + return err + } + + // modify permissions + if err := utils.ChmodRecursively(AppDir, 0o755); err != nil { + log.Errorf("chmod failed: %s", err) + return err + } + + return nil +} + +func getLatest() (*Latest, error) { + url := fmt.Sprintf("%s/latest.json?now=%d", BaseURL, time.Now().Unix()) + + resp, err := http.Get(url) + if err != nil { + log.Debugf("failed to request version: %v", err) + return nil, err + } + defer func() { + _ = resp.Body.Close() + }() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Errorf("failed to read response: %v", err) + return nil, err + } + + if resp.StatusCode != http.StatusOK { + log.Errorf("server responded with status code: %d", resp.StatusCode) + return nil, err + } + + var latest Latest + if err := json.Unmarshal(body, &latest); err != nil { + log.Errorf("failed to unmarshal response: %s", err) + return nil, err + } + + log.Debugf("get application latest version: %s", latest.Version) + return &latest, nil +} + +func download(url string, target string) (err error) { + for i := range maxTries { + log.Debugf("attempt #%d/%d", i+1, maxTries) + if i > 0 { + time.Sleep(time.Second * 3) // wait for 3 seconds before retrying the download attempt + } + + var req *http.Request + req, err = http.NewRequest("GET", url, nil) + if err != nil { + log.Errorf("new request err: %s", err) + continue + } + + log.Debugf("update will be saved to: %s", target) + err = utils.Download(req, target) + if err != nil { + log.Errorf("downloading latest application failed, try again...") + continue + } + return nil + } + return err +} + +func checksum(filePath string, expectedHash string) error { + file, err := os.Open(filePath) + if err != nil { + log.Errorf("failed to open file %s: %v", filePath, err) + return err + } + defer func() { + _ = file.Close() + }() + + hasher := sha512.New() + + _, err = io.Copy(hasher, file) + if err != nil { + log.Errorf("failed to copy file contents to hasher: %v", err) + return err + } + + hash := base64.StdEncoding.EncodeToString(hasher.Sum(nil)) + + if hash != expectedHash { + log.Errorf("invalid sha512 %s", hash) + return fmt.Errorf("invalid sha512 %s", hash) + } + + return nil +} diff --git a/server/utils/account.go b/server/service/auth/account.go similarity index 53% rename from server/utils/account.go rename to server/service/auth/account.go index 80d84f3..0305965 100644 --- a/server/utils/account.go +++ b/server/service/auth/account.go @@ -1,42 +1,28 @@ -package utils +package auth import ( + "NanoKVM-Server/utils" "encoding/json" "errors" "os" "path/filepath" log "github.com/sirupsen/logrus" + "golang.org/x/crypto/bcrypt" ) const AccountFile = "/etc/kvm/pwd" type Account struct { Username string `json:"username"` - Password string `json:"password"` -} - -func IsAccountExist() bool { - if _, err := os.Stat(AccountFile); err != nil { - if errors.Is(err, os.ErrNotExist) { - return false - } - return false - } - - return true + Password string `json:"password"` // should be named HashedPassword for clarity } func GetAccount() (*Account, error) { - // use default account if _, err := os.Stat(AccountFile); err != nil { if errors.Is(err, os.ErrNotExist) { - return &Account{ - Username: "admin", - Password: "admin", - }, nil + return getDefaultAccount(), nil } - return nil, err } @@ -46,26 +32,18 @@ func GetAccount() (*Account, error) { } var account Account - err = json.Unmarshal(content, &account) - if err != nil { + if err = json.Unmarshal(content, &account); err != nil { log.Errorf("unmarshal account failed: %s", err) return nil, err } - password, err := DecodeDecrypt(account.Password) - if err != nil { - return nil, err - } - - account.Password = password - return &account, nil } -func SetAccount(username string, password string) error { +func SetAccount(username string, hashedPassword string) error { account, err := json.Marshal(&Account{ Username: username, - Password: password, + Password: hashedPassword, }) if err != nil { log.Errorf("failed to marshal account information to json: %s", err) @@ -87,6 +65,35 @@ func SetAccount(username string, password string) error { return nil } +func CompareAccount(username string, plainPassword string) bool { + account, err := GetAccount() + if err != nil { + return false + } + + if username != account.Username { + return false + } + + hashedPassword, err := utils.DecodeDecrypt(plainPassword) + if err != nil || hashedPassword == "" { + return false + } + + err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(hashedPassword)) + if err != nil { + // Compatible with old versions + accountHashedPassword, _ := utils.DecodeDecrypt(account.Password) + if accountHashedPassword == hashedPassword { + return true + } + + return false + } + + return true +} + func DelAccount() error { if err := os.Remove(AccountFile); err != nil { log.Errorf("failed to delete password: %s", err) @@ -95,3 +102,12 @@ func DelAccount() error { return nil } + +func getDefaultAccount() *Account { + hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost) + + return &Account{ + Username: "admin", + Password: string(hashedPassword), + } +} diff --git a/server/service/auth/login.go b/server/service/auth/login.go index 7814359..8e9ba28 100644 --- a/server/service/auth/login.go +++ b/server/service/auth/login.go @@ -4,9 +4,6 @@ import ( "NanoKVM-Server/config" "NanoKVM-Server/middleware" "NanoKVM-Server/proto" - "NanoKVM-Server/utils" - "fmt" - "os" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" @@ -16,11 +13,6 @@ func (s *Service) Login(c *gin.Context) { var req proto.LoginReq var rsp proto.Response - if !isLibExist() { - rsp.ErrRsp(c, -6, "Lib not exist! Please connect to internet and update.") - return - } - // authentication disabled conf := config.GetInstance() if conf.Authentication == "disable" { @@ -35,26 +27,14 @@ func (s *Service) Login(c *gin.Context) { return } - passwordDecrypt, err := utils.DecodeDecrypt(req.Password) - if err != nil { - rsp.ErrRsp(c, -2, "decrypt password failed") - return - } - - account, err := utils.GetAccount() - if err != nil { - rsp.ErrRsp(c, -3, "get account failed") - return - } - - if req.Username != account.Username || passwordDecrypt != account.Password { - rsp.ErrRsp(c, -4, "invalid username or password") + if ok := CompareAccount(req.Username, req.Password); !ok { + rsp.ErrRsp(c, -2, "invalid username or password") return } token, err := middleware.GenerateJWT(req.Username) if err != nil { - rsp.ErrRsp(c, -5, "generate token failed") + rsp.ErrRsp(c, -3, "generate token failed") return } @@ -65,10 +45,21 @@ func (s *Service) Login(c *gin.Context) { log.Debugf("login success, username: %s", req.Username) } +func (s *Service) Logout(c *gin.Context) { + conf := config.GetInstance() + + if conf.JWT.RevokeTokensOnLogout { + config.RegenerateSecretKey() + } + + var rsp proto.Response + rsp.OkRsp(c) +} + func (s *Service) GetAccount(c *gin.Context) { var rsp proto.Response - account, err := utils.GetAccount() + account, err := GetAccount() if err != nil { rsp.ErrRsp(c, -1, "get account failed") return @@ -79,9 +70,3 @@ func (s *Service) GetAccount(c *gin.Context) { }) log.Debugf("get account successful") } - -func isLibExist() bool { - libPath := fmt.Sprintf("/kvmapp/kvm_system/dl_lib/libmaixcam_lib.so") - _, err := os.Stat(libPath) - return err == nil -} diff --git a/server/service/auth/password.go b/server/service/auth/password.go index 98ff20b..22c6016 100644 --- a/server/service/auth/password.go +++ b/server/service/auth/password.go @@ -3,6 +3,7 @@ package auth import ( "NanoKVM-Server/proto" "NanoKVM-Server/utils" + "errors" "io" "os" "os/exec" @@ -10,6 +11,7 @@ import ( "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" + "golang.org/x/crypto/bcrypt" ) func (s *Service) ChangePassword(c *gin.Context) { @@ -21,23 +23,28 @@ func (s *Service) ChangePassword(c *gin.Context) { return } - err := utils.SetAccount(req.Username, req.Password) - if err != nil { - rsp.ErrRsp(c, -2, "failed to save password") + password, err := utils.DecodeDecrypt(req.Password) + if err != nil || password == "" { + rsp.ErrRsp(c, -2, "invalid password") return } - account, err := utils.GetAccount() + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { - rsp.ErrRsp(c, -3, "failed to get password") + rsp.ErrRsp(c, -3, "failed to hash password") + return + } + + if err = SetAccount(req.Username, string(hashedPassword)); err != nil { + rsp.ErrRsp(c, -4, "failed to save password") return } // change root password - err = changeRootPassword(account.Password) + err = changeRootPassword(password) if err != nil { - _ = utils.DelAccount() - rsp.ErrRsp(c, -4, "failed to change password") + _ = DelAccount() + rsp.ErrRsp(c, -5, "failed to change password") return } @@ -48,24 +55,26 @@ func (s *Service) ChangePassword(c *gin.Context) { func (s *Service) IsPasswordUpdated(c *gin.Context) { var rsp proto.Response - isUpdated := false - - if utils.IsAccountExist() { - account, err := utils.GetAccount() - if err != nil { - rsp.ErrRsp(c, -1, "failed to get password") - return - } - - if account != nil && account.Password != "admin" { - isUpdated = true - } + if _, err := os.Stat(AccountFile); err != nil { + rsp.OkRspWithData(c, &proto.IsPasswordUpdatedRsp{ + IsUpdated: false, + }) + return } + account, err := GetAccount() + if err != nil || account == nil { + rsp.ErrRsp(c, -1, "failed to get password") + return + } + + err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte("admin")) + rsp.OkRspWithData(c, &proto.IsPasswordUpdatedRsp{ - IsUpdated: isUpdated, + // If the hash is not valid, still assume it's not updated + // The error we want to see is password and hash not matching + IsUpdated: errors.Is(err, bcrypt.ErrMismatchedHashAndPassword), }) - log.Debugf("is password updated: %t", isUpdated) } func changeRootPassword(password string) error { diff --git a/server/service/network/tailscale/cli.go b/server/service/extensions/tailscale/cli.go similarity index 95% rename from server/service/network/tailscale/cli.go rename to server/service/extensions/tailscale/cli.go index 2b68e08..1fa4cc9 100644 --- a/server/service/network/tailscale/cli.go +++ b/server/service/extensions/tailscale/cli.go @@ -69,11 +69,13 @@ func (c *Cli) Stop() error { return err } + _ = os.Remove(ConfigPath) + return os.Remove(ScriptPath) } func (c *Cli) Up() error { - command := "tailscale up" + command := "tailscale up --accept-dns=false" return exec.Command("sh", "-c", command).Run() } @@ -111,7 +113,7 @@ func (c *Cli) Status() (*TsStatus, error) { } func (c *Cli) Login() (string, error) { - command := "tailscale login --timeout=10m" + command := "tailscale login --accept-dns=false --timeout=10m" cmd := exec.Command("sh", "-c", command) stderr, err := cmd.StderrPipe() diff --git a/server/service/extensions/tailscale/install.go b/server/service/extensions/tailscale/install.go new file mode 100644 index 0000000..6ccebff --- /dev/null +++ b/server/service/extensions/tailscale/install.go @@ -0,0 +1,118 @@ +package tailscale + +import ( + "NanoKVM-Server/utils" + "fmt" + "io" + "net/http" + "os" + + log "github.com/sirupsen/logrus" +) + +const ( + OriginalURL = "https://pkgs.tailscale.com/stable/tailscale_latest_riscv64.tgz" + Workspace = "/root/.tailscale" +) + +func isInstalled() bool { + _, err1 := os.Stat(TailscalePath) + _, err2 := os.Stat(TailscaledPath) + + return err1 == nil && err2 == nil +} + +func install() error { + _ = os.MkdirAll(Workspace, 0o755) + defer func() { + _ = os.RemoveAll(Workspace) + }() + + tarFile := fmt.Sprintf("%s/tailscale_riscv64.tgz", Workspace) + + // download + if err := download(tarFile); err != nil { + log.Errorf("failed to download tailscale: %s", err) + return err + } + + // decompress + dir, err := utils.UnTarGz(tarFile, Workspace) + if err != nil { + log.Errorf("failed to decompress tailscale: %s", err) + return err + } + + // move + tailscalePath := fmt.Sprintf("%s/tailscale", dir) + err = utils.MoveFile(tailscalePath, TailscalePath) + if err != nil { + log.Errorf("failed to move tailscale: %s", err) + return err + } + + tailscaledPath := fmt.Sprintf("%s/tailscaled", dir) + err = utils.MoveFile(tailscaledPath, TailscaledPath) + if err != nil { + log.Errorf("failed to move tailscaled: %s", err) + return err + } + + log.Debugf("install tailscale successfully") + return nil +} + +func download(target string) error { + url, err := getDownloadURL() + if err != nil { + log.Errorf("failed to get Tailscale download url: %s", err) + return err + } + + resp, err := http.Get(url) + if err != nil { + log.Errorf("failed to download Tailscale: %s", err) + return err + } + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + out, err := os.Create(target) + if err != nil { + log.Errorf("failed to create file: %s", err) + return err + } + defer func() { + _ = out.Close() + }() + + _, err = io.Copy(out, resp.Body) + if err != nil { + log.Errorf("failed to copy response body to file: %s", err) + return err + } + + log.Debugf("download Tailscale successfully") + return nil +} + +func getDownloadURL() (string, error) { + resp, err := (&http.Client{}).Get(OriginalURL) + if err != nil { + return "", err + } + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusFound { + return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + return resp.Request.URL.String(), nil +} diff --git a/server/service/extensions/tailscale/service.go b/server/service/extensions/tailscale/service.go new file mode 100644 index 0000000..2881666 --- /dev/null +++ b/server/service/extensions/tailscale/service.go @@ -0,0 +1,235 @@ +package tailscale + +import ( + "NanoKVM-Server/proto" + "NanoKVM-Server/utils" + "net" + "os" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" +) + +type Service struct{} + +const ( + TailscalePath = "/usr/bin/tailscale" + TailscaledPath = "/usr/sbin/tailscaled" + ConfigPath = "etc/sysctl.d/99-tailscale.conf" +) + +var StateMap = map[string]proto.TailscaleState{ + "NoState": proto.TailscaleNotRunning, + "Starting": proto.TailscaleNotRunning, + "NeedsLogin": proto.TailscaleNotLogin, + "Running": proto.TailscaleRunning, + "Stopped": proto.TailscaleStopped, +} + +func NewService() *Service { + return &Service{} +} + +func (s *Service) Install(c *gin.Context) { + var rsp proto.Response + + if !isInstalled() { + if err := install(); err != nil { + rsp.ErrRsp(c, -1, "install failed") + return + } + } + + rsp.OkRsp(c) + log.Debugf("install tailscale successfully") +} + +func (s *Service) Uninstall(c *gin.Context) { + var rsp proto.Response + + _ = NewCli().Stop() + _ = utils.DelGoMemLimit() + + _ = os.Remove(TailscalePath) + _ = os.Remove(TailscaledPath) + _ = os.Remove(ConfigPath) + + rsp.OkRsp(c) + log.Debugf("uninstall tailscale successfully") +} + +func (s *Service) Start(c *gin.Context) { + var rsp proto.Response + + err := NewCli().Start() + if err != nil { + rsp.ErrRsp(c, -1, "start failed") + log.Errorf("failed to run tailscale start: %s", err) + return + } + + if !utils.IsGoMemLimitExist() { + _ = utils.SetGoMemLimit(50) + } + + rsp.OkRsp(c) + log.Debugf("tailscale start successfully") +} + +func (s *Service) Restart(c *gin.Context) { + var rsp proto.Response + + err := NewCli().Restart() + if err != nil { + rsp.ErrRsp(c, -1, "restart failed") + log.Errorf("failed to run tailscale restart: %s", err) + return + } + + rsp.OkRsp(c) + log.Debugf("tailscale restart successfully") +} + +func (s *Service) Stop(c *gin.Context) { + var rsp proto.Response + + err := NewCli().Stop() + if err != nil { + rsp.ErrRsp(c, -1, "stop failed") + log.Errorf("failed to run tailscale stop: %s", err) + return + } + + _ = utils.DelGoMemLimit() + + rsp.OkRsp(c) + log.Debugf("tailscale stop successfully") +} + +func (s *Service) Up(c *gin.Context) { + var rsp proto.Response + + err := NewCli().Up() + if err != nil { + rsp.ErrRsp(c, -1, "tailscale up failed") + log.Errorf("failed to run tailscale up: %s", err) + return + } + + rsp.OkRsp(c) + log.Debugf("run tailscale up successfully") +} + +func (s *Service) Down(c *gin.Context) { + var rsp proto.Response + + err := NewCli().Down() + if err != nil { + rsp.ErrRsp(c, -1, "tailscale down failed") + log.Errorf("failed to run tailscale down: %s", err) + return + } + + rsp.OkRsp(c) + log.Debugf("run tailscale down successfully") +} + +func (s *Service) Login(c *gin.Context) { + var rsp proto.Response + + // check tailscale status + cli := NewCli() + status, err := cli.Status() + if err != nil { + _ = cli.Start() + status, err = cli.Status() + } + + if err != nil { + log.Errorf("failed to get tailscale status: %s", err) + rsp.ErrRsp(c, -1, "unknown status") + return + } + + if status.BackendState == "Running" { + rsp.OkRspWithData(c, &proto.LoginTailscaleRsp{}) + return + } + + // get login url + url, err := cli.Login() + if err != nil { + log.Errorf("failed to run tailscale login: %s", err) + rsp.ErrRsp(c, -2, "login failed") + return + } + + if !utils.IsGoMemLimitExist() { + _ = utils.SetGoMemLimit(50) + } + + rsp.OkRspWithData(c, &proto.LoginTailscaleRsp{ + Url: url, + }) + + log.Debugf("tailscale login url: %s", url) +} + +func (s *Service) Logout(c *gin.Context) { + var rsp proto.Response + + err := NewCli().Logout() + if err != nil { + rsp.ErrRsp(c, -1, "logout failed") + log.Errorf("failed to run tailscale logout: %s", err) + return + } + + rsp.OkRsp(c) + log.Debugf("tailscale logout successfully") +} + +func (s *Service) GetStatus(c *gin.Context) { + var rsp proto.Response + + if !isInstalled() { + rsp.OkRspWithData(c, &proto.GetTailscaleStatusRsp{ + State: proto.TailscaleNotInstall, + }) + return + } + + status, err := NewCli().Status() + if err != nil { + log.Debugf("failed to get tailscale status: %s", err) + rsp.OkRspWithData(c, &proto.GetTailscaleStatusRsp{ + State: proto.TailscaleNotRunning, + }) + return + } + + state, ok := StateMap[status.BackendState] + if !ok { + log.Errorf("unknown tailscale state: %s", status.BackendState) + rsp.ErrRsp(c, -1, "unknown state") + return + } + + ipv4 := "" + for _, tailscaleIp := range status.Self.TailscaleIPs { + ip := net.ParseIP(tailscaleIp) + if ip != nil && ip.To4() != nil { + ipv4 = ip.String() + } + } + + data := proto.GetTailscaleStatusRsp{ + State: state, + IP: ipv4, + Name: status.Self.HostName, + Account: status.CurrentTailnet.Name, + } + + rsp.OkRspWithData(c, &data) + log.Debugf("get tailscale status successfully") +} diff --git a/server/service/network/service.go b/server/service/network/service.go index 1003f3e..6b02b6b 100644 --- a/server/service/network/service.go +++ b/server/service/network/service.go @@ -1,49 +1,7 @@ package network -import ( - "NanoKVM-Server/service/network/tailscale" - - "github.com/gin-gonic/gin" -) - type Service struct{} func NewService() *Service { return &Service{} } - -func (s *Service) TsInstall(c *gin.Context) { - tailscale.Install(c) -} - -func (s *Service) TsUninstall(c *gin.Context) { - tailscale.Uninstall(c) -} - -func (s *Service) GetTsStatus(c *gin.Context) { - tailscale.GetStatus(c) -} - -func (s *Service) TsUp(c *gin.Context) { - tailscale.Up(c) -} - -func (s *Service) TsDown(c *gin.Context) { - tailscale.Down(c) -} - -func (s *Service) TsLogin(c *gin.Context) { - tailscale.Login(c) -} - -func (s *Service) TsLogout(c *gin.Context) { - tailscale.Logout(c) -} - -func (s *Service) TsStop(c *gin.Context) { - tailscale.Stop(c) -} - -func (s *Service) TsRestart(c *gin.Context) { - tailscale.Restart(c) -} diff --git a/server/service/network/tailscale/account.go b/server/service/network/tailscale/account.go deleted file mode 100644 index 4a12f5c..0000000 --- a/server/service/network/tailscale/account.go +++ /dev/null @@ -1,87 +0,0 @@ -package tailscale - -import ( - "NanoKVM-Server/proto" - - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" -) - -func Login(c *gin.Context) { - var rsp proto.Response - - // check tailscale status - cli := NewCli() - status, err := cli.Status() - if err != nil { - _ = cli.Start() - status, err = cli.Status() - } - - if err != nil { - log.Errorf("failed to get tailscale status: %s", err) - rsp.ErrRsp(c, -1, "unknown status") - return - } - - if status.BackendState == "Running" { - rsp.OkRspWithData(c, &proto.LoginTailscaleRsp{}) - return - } - - // get login url - url, err := cli.Login() - if err != nil { - log.Errorf("failed to run tailscale login: %s", err) - rsp.ErrRsp(c, -2, "login failed") - return - } - - rsp.OkRspWithData(c, &proto.LoginTailscaleRsp{ - Url: url, - }) - - log.Debugf("tailscale login url: %s", url) -} - -func Logout(c *gin.Context) { - var rsp proto.Response - - err := NewCli().Logout() - if err != nil { - rsp.ErrRsp(c, -1, "logout failed") - log.Errorf("failed to run tailscale logout: %s", err) - return - } - - rsp.OkRsp(c) - log.Debugf("tailscale logout successfully") -} - -func Restart(c *gin.Context) { - var rsp proto.Response - - err := NewCli().Restart() - if err != nil { - rsp.ErrRsp(c, -1, "restart failed") - log.Errorf("failed to run tailscale restart: %s", err) - return - } - - rsp.OkRsp(c) - log.Debugf("tailscale restart successfully") -} - -func Stop(c *gin.Context) { - var rsp proto.Response - - err := NewCli().Stop() - if err != nil { - rsp.ErrRsp(c, -1, "stop failed") - log.Errorf("failed to run tailscale stop: %s", err) - return - } - - rsp.OkRsp(c) - log.Debugf("tailscale stop successfully") -} diff --git a/server/service/network/tailscale/install.go b/server/service/network/tailscale/install.go deleted file mode 100644 index 4174b7b..0000000 --- a/server/service/network/tailscale/install.go +++ /dev/null @@ -1,97 +0,0 @@ -package tailscale - -import ( - "NanoKVM-Server/proto" - "NanoKVM-Server/utils" - "fmt" - "net/http" - "os" - - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" -) - -const ( - DownloadUrl = "https://cdn.sipeed.com/nanokvm/resources/tailscale_riscv64.zip" - Workspace = "/root/.tailscale" - - TailscalePath = "/usr/bin/tailscale" - TailscaledPath = "/usr/sbin/tailscaled" -) - -func Install(c *gin.Context) { - var rsp proto.Response - - if IsInstalled() { - rsp.OkRsp(c) - return - } - - _ = os.MkdirAll(Workspace, 0o755) - defer func() { - _ = os.RemoveAll(Workspace) - }() - - // download - req, err := http.NewRequest("GET", DownloadUrl, nil) - if err != nil { - rsp.ErrRsp(c, -1, "request failed") - log.Errorf("failed to create request: %s", err) - return - } - - zipPath := fmt.Sprintf("%s/tailscale_riscv64.zip", Workspace) - err = utils.Download(req, zipPath) - if err != nil { - rsp.ErrRsp(c, -2, "download failed") - log.Errorf("failed to download tailscale: %s", err) - return - } - - // install - err = utils.Unzip(zipPath, Workspace) - if err != nil { - rsp.ErrRsp(c, -3, "unzip failed") - log.Errorf("failed to unzip tailscale: %s", err) - return - } - - tailscalePath := fmt.Sprintf("%s/tailscale_riscv64/tailscale", Workspace) - err = utils.MoveFile(tailscalePath, TailscalePath) - if err != nil { - rsp.ErrRsp(c, -4, "install failed") - log.Errorf("failed to move tailscale: %s", err) - return - } - - tailscaledPath := fmt.Sprintf("%s/tailscale_riscv64/tailscaled", Workspace) - err = utils.MoveFile(tailscaledPath, TailscaledPath) - if err != nil { - rsp.ErrRsp(c, -5, "install failed") - log.Errorf("failed to move tailscaled: %s", err) - return - } - - rsp.OkRsp(c) - log.Debugf("install tailscale successfully") -} - -func Uninstall(c *gin.Context) { - var rsp proto.Response - - _ = NewCli().Stop() - _ = utils.DelGoMemLimit() - - _ = os.Remove(TailscalePath) - _ = os.Remove(TailscaledPath) - - rsp.OkRsp(c) - log.Debugf("uninstall tailscale successfully") -} - -func IsInstalled() bool { - _, err1 := os.Stat(TailscalePath) - _, err2 := os.Stat(TailscaledPath) - - return err1 == nil && err2 == nil -} diff --git a/server/service/network/tailscale/status.go b/server/service/network/tailscale/status.go deleted file mode 100644 index 7141042..0000000 --- a/server/service/network/tailscale/status.go +++ /dev/null @@ -1,90 +0,0 @@ -package tailscale - -import ( - "NanoKVM-Server/proto" - "net" - - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" -) - -var StateMap = map[string]proto.TailscaleState{ - "NoState": proto.TailscaleNotRunning, - "Starting": proto.TailscaleNotRunning, - "NeedsLogin": proto.TailscaleNotLogin, - "Running": proto.TailscaleRunning, - "Stopped": proto.TailscaleStopped, -} - -func GetStatus(c *gin.Context) { - var rsp proto.Response - - if !IsInstalled() { - rsp.OkRspWithData(c, &proto.GetTailscaleStatusRsp{ - State: proto.TailscaleNotInstall, - }) - return - } - - status, err := NewCli().Status() - if err != nil { - log.Debugf("failed to get tailscale status: %s", err) - rsp.OkRspWithData(c, &proto.GetTailscaleStatusRsp{ - State: proto.TailscaleNotRunning, - }) - return - } - - state, ok := StateMap[status.BackendState] - if !ok { - log.Errorf("unknown tailscale state: %s", status.BackendState) - rsp.ErrRsp(c, -1, "unknown state") - return - } - - ipv4 := "" - for _, tailscaleIp := range status.Self.TailscaleIPs { - ip := net.ParseIP(tailscaleIp) - if ip != nil && ip.To4() != nil { - ipv4 = ip.String() - } - } - - data := proto.GetTailscaleStatusRsp{ - State: state, - IP: ipv4, - Name: status.Self.HostName, - Account: status.CurrentTailnet.Name, - } - - rsp.OkRspWithData(c, &data) - log.Debugf("get tailscale status successfully") -} - -func Up(c *gin.Context) { - var rsp proto.Response - - err := NewCli().Up() - if err != nil { - rsp.ErrRsp(c, -1, "tailscale up failed") - log.Errorf("failed to run tailscale up: %s", err) - return - } - - rsp.OkRsp(c) - log.Debugf("run tailscale up successfully") -} - -func Down(c *gin.Context) { - var rsp proto.Response - - err := NewCli().Down() - if err != nil { - rsp.ErrRsp(c, -1, "tailscale down failed") - log.Errorf("failed to run tailscale down: %s", err) - return - } - - rsp.OkRsp(c) - log.Debugf("run tailscale down successfully") -} diff --git a/server/service/storage/image.go b/server/service/storage/image.go index cd0884e..63360ed 100644 --- a/server/service/storage/image.go +++ b/server/service/storage/image.go @@ -1,11 +1,12 @@ package storage import ( - "errors" "os" "os/exec" "path/filepath" + "strconv" "strings" + "time" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" @@ -18,9 +19,7 @@ const ( imageNone = "/dev/mmcblk0p3" cdromFlag = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/cdrom" mountDevice = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/file" - removableFlag = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/removable" roFlag = "/sys/kernel/config/usb_gadget/g0/functions/mass_storage.disk0/lun.0/ro" - roDisk = "/boot/usb.disk0.ro" ) func (s *Service) GetImages(c *gin.Context) { @@ -61,55 +60,43 @@ func (s *Service) MountImage(c *gin.Context) { return } + // cdrom and ro flag + // set to 0 when unmount image + // set to 1 when mount image and the CD-ROM is enabled + if req.File == "" || req.Cdrom { + flag := "0" + if req.File != "" && req.Cdrom { + flag = "1" + } + + // unmount + if err := os.WriteFile(mountDevice, []byte("\n"), 0o666); err != nil { + log.Errorf("unmount file failed: %s", err) + rsp.ErrRsp(c, -2, "unmount image failed") + return + } + + // ro flag + if err := os.WriteFile(roFlag, []byte(flag), 0o666); err != nil { + log.Errorf("set ro flag failed: %s", err) + rsp.ErrRsp(c, -2, "set ro flag failed") + return + } + + // cdrom flag + if err := os.WriteFile(cdromFlag, []byte(flag), 0o666); err != nil { + log.Errorf("set cdrom flag failed: %s", err) + rsp.ErrRsp(c, -2, "set cdrom flag failed") + return + } + } + + // mount image := req.File if image == "" { image = imageNone } - imageRemovable := "1" - - imageRo := "0" - isImageRo, _ := isFlagExist(roDisk) - if isImageRo { - imageRo = "1" - } - - imageLow := strings.ToLower(image) - imageCdrom := "0" - if strings.HasSuffix(imageLow, ".iso") { - imageRo = "1" - imageCdrom = "1" - } - - // unmount - if err := os.WriteFile(mountDevice, []byte("\n"), 0o666); err != nil { - log.Errorf("unmount file failed: %s", err) - rsp.ErrRsp(c, -2, "unmount image failed") - return - } - - // removable flag - if err := os.WriteFile(removableFlag, []byte(imageRemovable), 0o666); err != nil { - log.Errorf("set removable flag failed: %s", err) - rsp.ErrRsp(c, -2, "set removable flag failed") - return - } - - // ro flag - if err := os.WriteFile(roFlag, []byte(imageRo), 0o666); err != nil { - log.Errorf("set ro flag failed: %s", err) - rsp.ErrRsp(c, -2, "set ro flag failed") - return - } - - // cdrom flag - if err := os.WriteFile(cdromFlag, []byte(imageCdrom), 0o666); err != nil { - log.Errorf("set cdrom flag failed: %s", err) - rsp.ErrRsp(c, -2, "set cdrom flag failed") - return - } - - // mount if err := os.WriteFile(mountDevice, []byte(image), 0o666); err != nil { log.Errorf("mount file %s failed: %s", image, err) rsp.ErrRsp(c, -2, "mount image failed") @@ -128,6 +115,7 @@ func (s *Service) MountImage(c *gin.Context) { rsp.ErrRsp(c, -2, "execute command failed") return } + time.Sleep(100 * time.Millisecond) } rsp.OkRsp(c) @@ -155,17 +143,25 @@ func (s *Service) GetMountedImage(c *gin.Context) { rsp.OkRspWithData(c, data) } -func isFlagExist(flag string) (bool, error) { - _, err := os.Stat(flag) +func (s *Service) GetCdRom(c *gin.Context) { + var rsp proto.Response - if err == nil { - return true, nil + content, err := os.ReadFile(cdromFlag) + if err != nil { + rsp.ErrRsp(c, -1, "read failed") + return } - if errors.Is(err, os.ErrNotExist) { - return false, nil + flag := strings.ReplaceAll(string(content), "\n", "") + flatInt, err := strconv.ParseInt(flag, 10, 64) + if err != nil { + rsp.ErrRsp(c, -2, "parse failed") + return } - log.Errorf("check file %s err: %s", flag, err) - return false, err + data := &proto.GetCdRomRsp{ + Cdrom: flatInt, + } + + rsp.OkRspWithData(c, data) } diff --git a/server/service/stream/frame-rate.go b/server/service/stream/frame-rate.go index a4ffed3..f5de521 100644 --- a/server/service/stream/frame-rate.go +++ b/server/service/stream/frame-rate.go @@ -42,7 +42,7 @@ func GetFrameRateCounter() *FrameRateCounter { data := fmt.Sprintf("%d", counter.fps) err := os.WriteFile("/kvmapp/kvm/now_fps", []byte(data), 0o666) if err != nil { - log.Errorf("failed to wirte fps: %s", err) + log.Errorf("failed to write fps: %s", err) } } }() diff --git a/server/service/stream/mjpeg/frame-detect.go b/server/service/stream/mjpeg/frame-detect.go index cb4fc8a..fa89358 100644 --- a/server/service/stream/mjpeg/frame-detect.go +++ b/server/service/stream/mjpeg/frame-detect.go @@ -93,11 +93,11 @@ func StopFrameDetect(c *gin.Context) { go func() { time.Sleep(20 * time.Second) _ = os.Rename(frameDetectTmp, frameDetect) - log.Debugf("frame detect started") + log.Debug("frame detect started") }() rsp.OkRsp(c) - log.Debugf("frame detect stoped") + log.Debug("frame detect stopped") } func isFrameDetectEnabled() (bool, error) { diff --git a/server/service/stream/mjpeg/mjpeg.go b/server/service/stream/mjpeg/mjpeg.go index 501694c..32ba210 100644 --- a/server/service/stream/mjpeg/mjpeg.go +++ b/server/service/stream/mjpeg/mjpeg.go @@ -3,7 +3,6 @@ package mjpeg import ( "NanoKVM-Server/common" "NanoKVM-Server/service/stream" - "fmt" "sync" "time" @@ -18,7 +17,7 @@ var ( ) func Connect(c *gin.Context) { - c.Header("Content-Type", fmt.Sprintf("multipart/x-mixed-replace; boundary=frame")) + c.Header("Content-Type", "multipart/x-mixed-replace; boundary=frame") c.Header("Cache-Control", "no-cache") c.Header("Connection", "keep-alive") c.Header("Pragma", "no-cache") diff --git a/server/service/vm/gpio.go b/server/service/vm/gpio.go index b9e0283..e34f606 100644 --- a/server/service/vm/gpio.go +++ b/server/service/vm/gpio.go @@ -46,7 +46,7 @@ func (s *Service) SetGpio(c *gin.Context) { return } - log.Debugf("gpio %s set sucessfully", device) + log.Debugf("gpio %s set successfully", device) rsp.OkRsp(c) } diff --git a/server/service/vm/hdmi.go b/server/service/vm/hdmi.go index 4b14db3..40a8e2c 100644 --- a/server/service/vm/hdmi.go +++ b/server/service/vm/hdmi.go @@ -14,9 +14,9 @@ func (s *Service) ResetHdmi(c *gin.Context) { vision := common.GetKvmVision() - vision.EnableHdmi(false) + vision.SetHDMI(false) time.Sleep(1 * time.Second) - vision.EnableHdmi(true) + vision.SetHDMI(true) rsp.OkRsp(c) log.Debug("reset hdmi") diff --git a/server/service/vm/info.go b/server/service/vm/info.go index fb9b32b..ebc92c7 100644 --- a/server/service/vm/info.go +++ b/server/service/vm/info.go @@ -18,6 +18,7 @@ var imageVersionMap = map[string]string{ "2024-07-23-20-18-587710.img": "v1.1.0", "2024-08-08-19-44-bef2ca.img": "v1.2.0", "2024-11-13-09-59-9c961a.img": "v1.3.0", + "2025-02-17-16-59-3649fe.img": "v1.4.0", } func (s *Service) GetInfo(c *gin.Context) { diff --git a/server/service/vm/memory.go b/server/service/vm/memory.go index fd350f5..9e7fa98 100644 --- a/server/service/vm/memory.go +++ b/server/service/vm/memory.go @@ -30,7 +30,7 @@ func (s *Service) SetMemoryLimit(c *gin.Context) { } rsp.OkRsp(c) - log.Debug("set memory limit successful, enabled: %t, limit: %s", req.Enabled, req.Limit) + log.Debugf("set memory limit successful, enabled: %t, limit: %d", req.Enabled, req.Limit) } func (s *Service) GetMemoryLimit(c *gin.Context) { diff --git a/server/service/vm/script.go b/server/service/vm/script.go index d16e3ee..9c93b96 100644 --- a/server/service/vm/script.go +++ b/server/service/vm/script.go @@ -112,7 +112,7 @@ func (s *Service) RunScript(c *gin.Context) { } if err != nil { - log.Errorf("run script %s faile: %s", req.Name, err) + log.Errorf("run script %s failed: %s", req.Name, err.Error()) rsp.ErrRsp(c, -2, "run script failed") return } diff --git a/server/service/vm/ssh.go b/server/service/vm/ssh.go new file mode 100644 index 0000000..fb854d4 --- /dev/null +++ b/server/service/vm/ssh.go @@ -0,0 +1,67 @@ +package vm + +import ( + "NanoKVM-Server/proto" + "errors" + "fmt" + "os" + "os/exec" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" +) + +const ( + SSHScript = "/etc/init.d/S50sshd" + SSHStopFlag = "/etc/kvm/ssh_stop" +) + +func (s *Service) GetSSHState(c *gin.Context) { + var rsp proto.Response + + enabled := isSSHEnabled() + rsp.OkRspWithData(c, &proto.GetSSHStateRsp{ + Enabled: enabled, + }) +} + +func (s *Service) EnableSSH(c *gin.Context) { + var rsp proto.Response + + command := fmt.Sprintf("%s permanent_on", SSHScript) + err := exec.Command("sh", "-c", command).Run() + if err != nil { + log.Errorf("failed to run SSH script: %s", err) + rsp.ErrRsp(c, -1, "operation failed") + return + } + + rsp.OkRsp(c) + log.Debugf("SSH enabled") +} + +func (s *Service) DisableSSH(c *gin.Context) { + var rsp proto.Response + + command := fmt.Sprintf("%s permanent_off", SSHScript) + err := exec.Command("sh", "-c", command).Run() + if err != nil { + log.Errorf("failed to run SSH script: %s", err) + rsp.ErrRsp(c, -1, "operation failed") + return + } + + rsp.OkRsp(c) + log.Debugf("SSH disabled") +} + +func isSSHEnabled() bool { + _, err := os.Stat(SSHStopFlag) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return true + } + } + + return false +} diff --git a/server/service/vm/terminal.go b/server/service/vm/terminal.go index 14775ed..0b6f08e 100644 --- a/server/service/vm/terminal.go +++ b/server/service/vm/terminal.go @@ -2,17 +2,15 @@ package vm import ( "encoding/json" - "io" "net/http" "os" + "os/exec" "time" + "github.com/creack/pty" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" log "github.com/sirupsen/logrus" - "golang.org/x/crypto/ssh" - - "NanoKVM-Server/utils" ) const ( @@ -20,244 +18,93 @@ const ( maxMessageSize = 1024 ) -type WindowSize struct { - Height int `json:"height"` - Width int `json:"width"` -} - -type SshClient struct { - conn *websocket.Conn - addr string - user string - password string - client *ssh.Client - session *ssh.Session - sessionIn io.WriteCloser - sessionOut io.Reader - closeSig chan struct{} +type WinSize struct { + Rows uint16 `json:"rows"` + Cols uint16 `json:"cols"` } var upgrader = websocket.Upgrader{ ReadBufferSize: maxMessageSize, WriteBufferSize: maxMessageSize, + CheckOrigin: func(r *http.Request) bool { + return true + }, } -var terminalModes = ssh.TerminalModes{ - ssh.ECHO: 1, - ssh.TTY_OP_ISPEED: 14400, - ssh.TTY_OP_OSPEED: 14400, -} - -func (s *SshClient) getWindowSize() (size *WindowSize, err error) { - _ = s.conn.SetReadDeadline(time.Now().Add(messageWait)) - - msgType, msg, err := s.conn.ReadMessage() +func (s *Service) Terminal(c *gin.Context) { + ws, err := upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { - log.Errorf("ws read message failed: %s", err) + log.Errorf("failed to init websocket: %s", err) return } - if msgType != websocket.BinaryMessage { - log.Errorf("ws message type is not binary") - return - } - - size = new(WindowSize) - if err = json.Unmarshal(msg, size); err != nil { - log.Errorf("unmarshal ws message failed: %s", err) - return - } - - return -} - -func (s *SshClient) wsWrite() error { defer func() { - s.closeSig <- struct{}{} + _ = ws.Close() }() + cmd := exec.Command("/bin/sh") + ptmx, err := pty.Start(cmd) + if err != nil { + log.Errorf("failed to start pty: %s", err) + return + } + defer func() { + _ = ptmx.Close() + _ = cmd.Process.Kill() + }() + + go wsWrite(ws, ptmx) + wsRead(ws, ptmx) +} + +// pty to ws +func wsWrite(ws *websocket.Conn, ptmx *os.File) { data := make([]byte, maxMessageSize) for { - time.Sleep(10 * time.Millisecond) - - n, err := s.sessionOut.Read(data) + n, err := ptmx.Read(data) if err != nil { - return err + return } if n > 0 { - _ = s.conn.SetWriteDeadline(time.Now().Add(messageWait)) + _ = ws.SetWriteDeadline(time.Now().Add(messageWait)) - err = s.conn.WriteMessage(websocket.BinaryMessage, data[:n]) + err = ws.WriteMessage(websocket.BinaryMessage, data[:n]) if err != nil { log.Errorf("write ws message failed: %s", err) - return err + return } } } } -func (s *SshClient) wsRead() error { - defer func() { - s.closeSig <- struct{}{} - }() - +// ws to pty +func wsRead(ws *websocket.Conn, ptmx *os.File) { var zeroTime time.Time - _ = s.conn.SetReadDeadline(zeroTime) + _ = ws.SetReadDeadline(zeroTime) for { - msgType, connReader, err := s.conn.NextReader() + msgType, p, err := ws.ReadMessage() if err != nil { - return err + return } - if msgType != websocket.BinaryMessage { - _, err = io.Copy(s.sessionIn, connReader) - if err != nil { - log.Errorf("copy ws message failed: %s", err) - return err + + // resize message + if msgType == websocket.BinaryMessage { + var winSize WinSize + if err := json.Unmarshal(p, &winSize); err == nil { + _ = pty.Setsize(ptmx, &pty.Winsize{ + Rows: winSize.Rows, + Cols: winSize.Cols, + }) } continue } - data := make([]byte, maxMessageSize) - n, err := connReader.Read(data) + _, err = ptmx.Write(p) if err != nil { - log.Errorf("read ws message failed: %s", err) - return err - } - - var size WindowSize - err = json.Unmarshal(data[:n], &size) - if err != nil { - log.Errorf("unmarshal ws message failed: %s", err) - return err - } - - err = s.session.WindowChange(size.Height, size.Width) - if err != nil { - log.Errorf("ws window change failed: %s", err) - return err + log.Errorf("failed to write to pty: %s", err) + return } } } - -func (s *SshClient) bridgeWSAndSSH() { - defer func() { - _ = s.conn.Close() - if r := recover(); r != nil { - log.Debugf("terminal recover: %s", r) - } - }() - - size, err := s.getWindowSize() - if err != nil { - return - } - - auth := ssh.Password(s.password) - config := &ssh.ClientConfig{ - User: s.user, - Auth: []ssh.AuthMethod{auth}, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - } - - s.client, err = ssh.Dial("tcp", s.addr, config) - if err != nil { - // log.Errorf("init ssh failed: %s", err) - return - } - defer func() { - _ = s.client.Close() - }() - - s.session, err = s.client.NewSession() - if err != nil { - log.Errorf("open ssh session failed: %s", err) - return - } - defer s.session.Close() - - s.session.Stderr = os.Stderr - s.sessionOut, err = s.session.StdoutPipe() - if err != nil { - log.Errorf("open ssh session out failed: %s", err) - return - } - - s.sessionIn, err = s.session.StdinPipe() - if err != nil { - log.Errorf("open ssh session in failed: %s", err) - return - } - defer func() { - _ = s.sessionIn.Close() - }() - - if err = s.session.RequestPty("xterm", size.Height, size.Width, terminalModes); err != nil { - log.Errorf("oen session request pty failed: %s", err) - return - } - if err = s.session.Shell(); err != nil { - log.Errorf("open ssh shell failed: %s", err) - return - } - - go func() { - _ = s.wsRead() - }() - - go func() { - _ = s.wsWrite() - }() - - <-s.closeSig -} - -func getRootPassword() string { - if !utils.IsAccountExist() { - return "root" - } - - account, err := utils.GetAccount() - if err != nil { - return "root" - } - - if account == nil || account.Password == "" { - return "root" - } - - return account.Password -} - -func (s *Service) Terminal(c *gin.Context) { - user := c.Query("u") - if user == "" { - user = "root" - } - - password, _ := utils.Decrypt(c.Query("t")) - if password == "" { - password = getRootPassword() - } - - upgrader.CheckOrigin = func(r *http.Request) bool { - return true - } - - conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) - if err != nil { - log.Errorf("init websocket failed: %s", err) - return - } - - sshClient := &SshClient{ - conn: conn, - addr: "127.0.0.1:22", - user: user, - password: password, - closeSig: make(chan struct{}, 1), - } - - go sshClient.bridgeWSAndSSH() -} diff --git a/server/service/ws/ws.go b/server/service/ws/ws.go index 25bc010..d24ea5a 100644 --- a/server/service/ws/ws.go +++ b/server/service/ws/ws.go @@ -71,12 +71,12 @@ func (c *WsClient) Read() error { return err } - log.Debugf("receive message: %s", message) + log.Debugf("received message: %s", message) var event []int err = json.Unmarshal(message, &event) if err != nil { - log.Debugf("receive invalid message: %s", message) + log.Debugf("received invalid message: %s", message) continue } diff --git a/server/utils/encrypt.go b/server/utils/encrypt.go index 9b6af58..ad94d97 100644 --- a/server/utils/encrypt.go +++ b/server/utils/encrypt.go @@ -7,30 +7,24 @@ import ( log "github.com/sirupsen/logrus" ) -const EncryptSecretKey = "nanokvm-sipeed-2024" +// SecretKey is only used to prevent the data from being transmitted in plaintext. +const SecretKey = "nanokvm-sipeed-2024" func Decrypt(ciphertext string) (string, error) { - defer func() { - if err := recover(); err != nil { - log.Errorf("decrypt failed: %s", err) - } - }() - if ciphertext == "" { return "", nil } - decrypt := aes256.Decrypt(ciphertext, EncryptSecretKey) - + decrypt := aes256.Decrypt(ciphertext, SecretKey) return decrypt, nil } -func DecodeDecrypt(ciphertext string) (string, error) { - decode, err := url.QueryUnescape(ciphertext) +func DecodeDecrypt(data string) (string, error) { + ciphertext, err := url.QueryUnescape(data) if err != nil { log.Errorf("decode ciphertext failed: %s", err) return "", err } - return Decrypt(decode) + return Decrypt(ciphertext) } diff --git a/server/utils/http.go b/server/utils/http.go index cb45891..8f77fc7 100644 --- a/server/utils/http.go +++ b/server/utils/http.go @@ -41,8 +41,8 @@ func Download(req *http.Request, target string) error { } contentType := resp.Header.Get("Content-Type") - if contentType != "application/octet-stream" && contentType != "application/zip" { - log.Debugf("unexpected content-type, it should be either octet-stream or zip, but got: %s", contentType) + if contentType != "application/octet-stream" && contentType != "application/zip" && contentType != "application/gzip" { + log.Debugf("unexpected content-type, it should be either octet-stream or (g)zip, but got: %s", contentType) return errors.New("unsupported content type") } diff --git a/server/utils/untar.go b/server/utils/untar.go new file mode 100644 index 0000000..26342c6 --- /dev/null +++ b/server/utils/untar.go @@ -0,0 +1,82 @@ +package utils + +import ( + "archive/tar" + "compress/gzip" + "io" + "os" + "path/filepath" + "strings" +) + +func UnTarGz(srcFile string, destDir string) (string, error) { + if err := os.MkdirAll(destDir, 0755); err != nil { + return "", err + } + + fr, err := os.Open(srcFile) + if err != nil { + return "", err + } + defer func() { + _ = fr.Close() + }() + + gr, err := gzip.NewReader(fr) + if err != nil { + return "", err + } + defer func() { + _ = gr.Close() + }() + + tr := tar.NewReader(gr) + + targetFile := "" + for { + header, err := tr.Next() + + if err == io.EOF { + break + } + + if err != nil { + return "", err + } + + if targetFile == "" { + parts := strings.Split(header.Name, "/") + if len(parts) > 0 { + targetFile = filepath.Join(destDir, parts[0]) + } + } + + filename := filepath.Join(destDir, header.Name) + + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(filename, os.FileMode(header.Mode)); err != nil { + return "", err + } + + case tar.TypeReg: + file, err := os.OpenFile(filename, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + if err != nil { + return "", err + } + + if _, err := io.Copy(file, tr); err != nil { + _ = file.Close() + return "", err + } + _ = file.Close() + + case tar.TypeSymlink: + if err := os.Symlink(header.Linkname, filename); err != nil { + return "", err + } + } + } + + return targetFile, nil +} diff --git a/support/kvm_system/README.md b/support/kvm_system/README.md new file mode 100644 index 0000000..e69de29 diff --git a/support/kvm_system/main/lib/system_init/system_init.cpp b/support/kvm_system/main/lib/system_init/system_init.cpp index 8102fc1..95af168 100644 --- a/support/kvm_system/main/lib/system_init/system_init.cpp +++ b/support/kvm_system/main/lib/system_init/system_init.cpp @@ -8,9 +8,9 @@ using namespace maix::sys; extern kvm_sys_state_t kvm_sys_state; extern kvm_oled_state_t kvm_oled_state; -void new_app_init(uint8_t safe_update) +void new_app_init(void) { - // first start + // Update the necessary scripts system("cp -f /kvmapp/system/update-nanokvm.py /etc/kvm/"); system("rm /etc/init.d/S02udisk"); system("cp /kvmapp/system/init.d/S01fs /etc/init.d/"); @@ -18,18 +18,26 @@ void new_app_init(uint8_t safe_update) system("cp /kvmapp/system/init.d/S15kvmhwd /etc/init.d/"); system("cp /kvmapp/system/init.d/S30eth /etc/init.d/"); system("cp /kvmapp/system/init.d/S50sshd /etc/init.d/"); - if(kvm_wifi_exist()) system("cp /kvmapp/system/init.d/S30wifi /etc/init.d/"); - else system("rm /etc/init.d/S30wifi"); - system("cp /kvmapp/system/init.d/S98tailscaled /etc/init.d/"); - if(safe_update == 0){ - system("cp /kvmapp/system/init.d/S95nanokvm /etc/init.d/"); // /kvmapp/system/init.d/S95nanokvm还没跑完直接覆盖可能出问题 + if(kvm_wifi_exist()) { + system("cp /kvmapp/system/init.d/S30wifi /etc/init.d/"); + } else { + system("rm /etc/init.d/S30wifi"); } - // system("rm /etc/init.d/S04backlight"); - // system("rm /etc/init.d/S05tp"); - // system("rm /etc/init.d/S40bluetoothd"); - // system("rm /etc/init.d/S99*"); + // PCIe Patch + // system("cp /kvmapp/system/init.d/S95nanokvm /etc/init.d/"); + if(access("/kvmapp/jpg_stream/dl_lib/libmaixcam_lib.so", F_OK) != 0){ + system("cp /kvmapp/system/init.d/S95nanokvm /etc/init.d/"); + } + + // Remove unnecessary components to speed up boot time + system("rm /etc/init.d/S04backlight"); + system("rm /etc/init.d/S05tp"); + system("rm /etc/init.d/S40bluetoothd"); + system("rm /etc/init.d/S50ssdpd"); + system("rm /etc/init.d/S99*"); + // Add necessary configuration files for program execution system("mkdir /kvmapp/kvm"); system("mkdir /etc/kvm"); system("echo 0 > /kvmapp/kvm/now_fps"); @@ -42,9 +50,8 @@ void new_app_init(uint8_t safe_update) // rm jpg_stream & kvm_stream system("rm -r /kvmapp/jpg_stream"); - system("rm /kvmapp/kvm_system/kvm_stream"); + system("rm /kvmapp/kvm_system/kvm_stream"); // Cannot delete temporarily; the old production test script uses this file to determine if the download is complete - // rm kvm_new_app system("rm /kvmapp/kvm_new_app"); system("sync"); // system("/etc/init.d/S95nanokvm restart"); @@ -106,9 +113,15 @@ void new_app_init(uint8_t safe_update) if(access("/root/old/kvm_new_img", F_OK) == 0){ system("rm -r /root/old"); } + + system("sync"); + system("killall NanoKVM-Server"); + system("rm -r /tmp/server"); + system("cp -r /kvmapp/server /tmp/"); + system("/tmp/server/NanoKVM-Server &"); } -void update_resolv_conf(void) +void build_complete_resolv(void) { FILE *fp = NULL; // fp = fopen("/boot/resolv.conf", "w+"); @@ -122,121 +135,8 @@ void update_resolv_conf(void) system("cp -vf /boot/resolv.conf /etc/resolv.conf"); } -void init_upadte(void) +void new_img_init(void) { - if(access("/kvmapp/kvm_new_app", F_OK) == 0){ - new_app_init(0); - } - - // PCIe Patch - if(access("/kvmapp/jpg_stream/dl_lib/libmaixcam_lib.so", F_OK) == 0){ - system("cp -f /kvmapp/jpg_stream/dl_lib/libmaixcam_lib.so /kvmapp/kvm_system/dl_lib/"); - new_app_init(1); - } - - // DNS Patch - system("tailscale set --accept-dns=false"); - if(access("/boot/resolv.conf", F_OK) != 0){ - // 不存在直接覆盖 - update_resolv_conf(); - } else { - // 存在则检测是否拷贝/DNS是否正确 - uint8_t RW_Data[10] = {0}; - FILE *fp; - fp = popen("cat /etc/resolv.conf | grep 119.29.29.29", "r"); - fgets((char*)RW_Data, 2, fp); - pclose(fp); - if(RW_Data[0] != 'n') { - update_resolv_conf(); - } - } - - // libmaixcam_lib.so - if(access("/kvmapp/kvm_system/dl_lib/libmaixcam_lib.so", F_OK) == 0){ - // exist - system("cp -f /kvmapp/kvm_system/dl_lib/libmaixcam_lib.so /kvmapp/server/dl_lib"); - } else { - printf("kvm_system not exist lib!\n"); - } - system("sync"); - system("killall NanoKVM-Server"); - system("rm -r /tmp/server"); - system("cp -r /kvmapp/server /tmp/"); - system("/tmp/server/NanoKVM-Server &"); -} - -void first_start(void) -{ - if(access("/kvmapp/kvm_new_img", F_OK) == 0){ - // init oled - OLED_state = oled_exist(); - if(OLED_state){ - printf("oled exist\r\n"); - } else { - printf("oled not exist\r\n"); - } - if(OLED_state){ - OLED_Init(); - OLED_ColorTurn(0); //0正常显示 1 反色显示 - OLED_DisplayTurn(0); //0正常显示 1 屏幕翻转显示 - // OLED_INIT_UI(); - - if(kvm_hw_ver == 2){ - OLED_Revolve(); - } - } - uint8_t update_begin = 0; - while(1){ - if(kvm_sys_state.eth_state == NIC_STATE_UP){ - OLED_Clear(); - if(kvm_hw_ver != 2){ - if(update_begin == 0){ - update_begin = 1; - OLED_ShowString(1, 2, "Auto-updating...", 16); - OLED_ShowStringtoend(1, 4, (char*)kvm_sys_state.eth_addr, 16, 0); - system("python /kvmapp/system/update-nanokvm.py"); - } else { - OLED_ShowString(1, 0, "Auto-update fail", 16); - OLED_ShowString(1, 2, "Please open the ", 16); - OLED_ShowStringtoend(1, 4, (char*)kvm_sys_state.eth_addr, 16, 0); - OLED_ShowString(1, 6, "update KVM APP ", 16); - } - } else { - // PCIe - if(update_begin == 0){ - update_begin = 1; - OLED_ShowString(0, 1, "Updating...", 8); - OLED_ShowString_AlignRight(AlignRightEND_P, 2, (char*)kvm_sys_state.eth_addr, 4); - system("python /kvmapp/system/update-nanokvm.py"); - } else { - OLED_ShowString(0, 0, "Update fail", 8); - OLED_ShowString(0, 1, "Please open", 8); - OLED_ShowString_AlignRight(AlignRightEND_P, 2, (char*)kvm_sys_state.eth_addr, 4); - OLED_ShowString(0, 3, "update APP" , 8); - } - } - while(1){ - time::sleep_ms(1000); - if(kvm_sys_state.eth_state == NIC_STATE_DOWN) { - OLED_Clear(); - break; - } - } - } else { - get_ip_addr(ETH_ROUTE); - if(kvm_hw_ver != 2){ - OLED_ShowString(1, 2, " Please connect ", 16); - OLED_ShowString(1, 4, " to the network ", 16); - OLED_ShowString(1, 6, " to update app ", 16); - } else { - // PCIe - OLED_ShowString(0, 0, "Please link", 8); - OLED_ShowString(0, 1, "to network", 8); - OLED_ShowString(0, 2, "update APP" , 8); - } - - time::sleep_ms(10); - } - } - } + build_complete_resolv(); + system("rm /kvmapp/kvm_new_img"); } \ No newline at end of file diff --git a/support/kvm_system/main/lib/system_init/system_init.h b/support/kvm_system/main/lib/system_init/system_init.h index b0a9e7c..9f84f17 100644 --- a/support/kvm_system/main/lib/system_init/system_init.h +++ b/support/kvm_system/main/lib/system_init/system_init.h @@ -2,9 +2,8 @@ #define SYSTEM_INIT_H_ #include "config.h" -void new_app_init(uint8_t safe_update); -void update_resolv_conf(void); void init_upadte(void); -void first_start(void); +void new_img_init(void); +void new_app_init(void); #endif // SYSTEM_INIT_H_ diff --git a/support/kvm_system/main/src/main.cpp b/support/kvm_system/main/src/main.cpp index 2476842..31c010d 100644 --- a/support/kvm_system/main/src/main.cpp +++ b/support/kvm_system/main/src/main.cpp @@ -186,9 +186,17 @@ int main(int argc, char* argv[]) pthread_t display_thread; pthread_t key_thread; - // first_start(); + // Execute only when it's a new image + if(access("/kvmapp/kvm_new_img", F_OK) == 0){ + new_img_init(); + } - init_upadte(); + // Execute only when it's a new app + if(access("/kvmapp/kvm_new_app", F_OK) == 0){ + new_app_init(); + } + + system("sync"); OLED_state = oled_exist(); if(OLED_state){ diff --git a/vision/components/kvm/CMakeLists.txt b/vision/components/kvm/CMakeLists.txt new file mode 100644 index 0000000..a97185c --- /dev/null +++ b/vision/components/kvm/CMakeLists.txt @@ -0,0 +1,6 @@ +list(APPEND ADD_REQUIREMENTS vision basic peripheral) +list(APPEND ADD_INCLUDE "include") + +append_srcs_dir(ADD_SRCS "src") + +register_component(DYNAMIC) \ No newline at end of file diff --git a/vision/components/kvm/Kconfig b/vision/components/kvm/Kconfig new file mode 100644 index 0000000..e69de29 diff --git a/vision/components/kvm/include/kvm_vision.h b/vision/components/kvm/include/kvm_vision.h new file mode 100644 index 0000000..8e10fa7 --- /dev/null +++ b/vision/components/kvm/include/kvm_vision.h @@ -0,0 +1,71 @@ + + +#ifndef KVM_VISION_H_ +#define KVM_VISION_H_ + +#include "kvm_mmf.hpp" +#include "maix_camera.hpp" +#include "maix_basic.hpp" + +#include "maix_i2c.hpp" + +#ifdef __cplusplus +extern "C" { +#endif +#include /* low-level i/o */ +#include +#include +#include +#include +#include +#include + +// #include "kvm_mmf.hpp" + +#define IMG_BUFFER_FULL -3 +#define IMG_VENC_ERROR -2 +#define IMG_NOT_EXIST -1 +#define IMG_MJPEG_TYPE 0 +#define IMG_H264_TYPE_SPS 1 +#define IMG_H264_TYPE_PPS 2 +#define IMG_H264_TYPE_IF 3 +#define IMG_H264_TYPE_PF 4 + +void kvmv_init(uint8_t _debug_info_en = 0); +/********************************************************************************** + * @name kvmv_read_img + * @author Sipeed BuGu + * @date 2024/10/25 + * @version R1.0 + * @brief Acquire the encoded image with auto init + * @param _width @input: Output image width + * @param _height @input: Output image height + * @param _type @input: Encode type + * @param _qlty @input: MJPEG: (50-100) | H264: (500-10000) + * @param _pp_kvm_data @output: Encode data + * @param _p_kvmv_data_size @output: Encode data size + * @return + -4: Modifying image resolution, please wait + -3: img buffer full + -2: VENC Error + -1: No images were acquired + 0: Acquire MJPEG encoded images + 1: Acquire H264 encoded images(SPS) + 2: Acquire H264 encoded images(PPS) + 3: Acquire H264 encoded images(I) + 4: Acquire H264 encoded images(P) + **********************************************************************************/ +int kvmv_read_img(uint16_t _width, uint16_t _height, uint8_t _type, uint16_t _qlty, uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size); +int kvmv_get_sps_frame(uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size); +int kvmv_get_pps_frame(uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size); +int free_kvmv_data(uint8_t ** _pp_kvm_data); +void free_all_kvmv_data(); +void set_h264_gop(uint8_t _gop); +void kvmv_deinit(); +uint8_t kvmv_hdmi_control(uint8_t _en); + +#ifdef __cplusplus +} +#endif + +#endif // KVM_VISION_H_ \ No newline at end of file diff --git a/vision/components/kvm/src/kvm_vision.cpp b/vision/components/kvm/src/kvm_vision.cpp new file mode 100644 index 0000000..5d8f652 --- /dev/null +++ b/vision/components/kvm/src/kvm_vision.cpp @@ -0,0 +1,1594 @@ +/** + * 待解决的问题: + * // 分辨率跟随输出 + * // 自动切换分辨率 + * // HDMI分辨率问题 + * // H264输入空图,VENC容易炸问题 + * // MJPEG/H264切换时卡退 + * // 再加一条手动清空全部内存 + * // deinit时free全部内存 + * // free 错内存时会炸的问题 + */ +#include "kvm_vision.h" + +#define default_venc_chn 1 + +#define VENC_MJPEG 0 +#define VENC_H264 1 + +#define KVMV_MAX_TRY_NUM 2 +#define vi_min_width 32 +#define vi_min_height 3 + +#define default_vi_width 1920 +#define default_vi_height 1080 +#define default_vpss_width 1920 +#define default_vpss_height 1080 +#define default_venc_type VENC_MJPEG +#define default_mjpeg_qlty 60 +#define default_h264_qlty 1000 +#define default_h264_gop 30 + +#define kvmv_data_buffer_size 4 + +#define vi_width_path "/kvmapp/kvm/width" +#define vi_height_path "/kvmapp/kvm/height" +#define hdmi_mode_path "/etc/kvm/hdmi_mode" +#define hdmi_state_path "/proc/lt_int" + +#define LT6911_ADDR 0x2B +#define LT6911_READ 0xFF +#define LT6911_WRITE 0x00 + +static char NanoKVM_edit[] = { + 0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0x41,0x0C,0x33,0xC2,0x66,0xBA,0x00,0x00, + 0x2B,0x1F,0x01,0x04,0xA5,0x50,0x22,0x78,0x3B,0xCC,0xE5,0xAB,0x51,0x48,0xA6,0x26, + 0x0C,0x50,0x54,0xBF,0xEF,0x00,0xD1,0xC0,0xB3,0x00,0x95,0x00,0x81,0x80,0x81,0x40, + 0x81,0xC0,0x01,0x01,0x01,0x01,0xD8,0x59,0x00,0x60,0xA3,0x38,0x28,0x40,0xA0,0x10, + 0x3A,0x10,0x20,0x4F,0x31,0x00,0x00,0x1A,0x00,0x00,0x00,0xFF,0x00,0x55,0x4B,0x30, + 0x32,0x31,0x34,0x33,0x30,0x34,0x37,0x37,0x31,0x38,0x00,0x00,0x00,0xFC,0x00,0x50, + 0x48,0x4C,0x20,0x33,0x34,0x32,0x45,0x32,0x0A,0x20,0x20,0x20,0x00,0x00,0x00,0xFD, + 0x00,0x30,0x4B,0x63,0x63,0x1E,0x01,0x0A,0x20,0x20,0x20,0x20,0x20,0x20,0x01,0x8E +}; + +using namespace maix; +using namespace maix::sys; +using namespace maix::peripheral; +i2c::I2C LT6911_i2c(4, i2c::Mode::MASTER); + +typedef struct { + uint16_t vi_width = default_vi_width; + uint16_t vi_height = default_vi_height; + uint16_t vpss_width = default_vpss_width; + uint16_t vpss_height = default_vpss_height; + uint8_t venc_type; + uint16_t qlty; + uint8_t cam_state = 0; + uint8_t stream_stop; + uint8_t frame_detact; + uint8_t display; + uint8_t reinit_flag = 1; + uint8_t reopen_cam_flag = 0; + uint8_t hdmi_cable_state = 0; + uint8_t try_exit_thread = 0; + uint8_t thread_is_running = 0; + uint8_t Auto_res = 0; + uint8_t hdmi_version = 0; + uint8_t hw_version = 0; + uint8_t hdmi_stop_flag = 0; + uint8_t hdmi_reading_flag = 0; + uint8_t hdmi_mode = 0; + uint8_t vi_detect_state = 0; +} kvmv_cfg_t; + +typedef struct { + uint8_t* p_img_data = NULL; + uint8_t img_data_type = 0; + uint32_t img_data_size = 0; +} kvmv_data_t; + +typedef struct { + uint8_t mmf_venc_chn; + uint8_t enc_h264_running; + uint8_t enc_h264_init; + mmf_venc_cfg_t kvm_venc_cfg; +} kvm_venc_t; + +camera::Camera *cam = new camera::Camera(default_vpss_width, default_vpss_height, image::FMT_YVU420SP); +// camera::Camera *cam = new camera::Camera(320, 240, image::FMT_RGB888); + +kvmv_cfg_t kvmv_cfg; + +kvmv_data_t kvmv_data_buffer[kvmv_data_buffer_size]; +kvmv_data_t kvmv_SPS_buffer = {0}; +kvmv_data_t kvmv_PPS_buffer = {0}; + +uint8_t kvmv_data_buffer_index = 0; + +uint8_t debug_en = 0; +void debug(const char *format, ...) +{ + if(debug_en){ + printf(format); + } +} + +uint8_t to_roll(int8_t _input) +{ + if(_input < 0) return _input + kvmv_data_buffer_size; + if(_input >= kvmv_data_buffer_size) return _input - kvmv_data_buffer_size; + return _input; +} + +int maxmin_data(int _max, int _min, int _data) +{ + if(_data > _max) return _max; + if(_data < _min) return _min; + return _data; +} + +kvmv_data_t* get_save_buffer() +{ + kvmv_data_buffer_index = to_roll(kvmv_data_buffer_index + 1); + // debug("[kvmv]kvmv_data_buffer_index = %d\n", kvmv_data_buffer_index); + // debug("[kvmv]kvmv_data_buffer.p_img_data = %d\n", kvmv_data_buffer[kvmv_data_buffer_index].p_img_data); + if(kvmv_data_buffer[kvmv_data_buffer_index].p_img_data == NULL){ + return &kvmv_data_buffer[kvmv_data_buffer_index]; + } + return NULL; +} + +// ====HDMI RES================================================== + +uint16_t hdmi_res_list[][2] = { + {1920, 1080}, + {1600, 900}, + {1440, 1080}, + {1440, 900}, + {1280, 1024}, + {1280, 960}, + {1280, 800}, + {1280, 720}, + {1152, 864}, + {1024, 768}, + {800, 600}, +}; + +/* return 0 : VI not init; + * return 1 : HDMI and CSI status are normal; + * return 2 : HDMI abnormal; + * return 3 : CSI abnormal: width too small; + * return 4 : CSI abnormal: width too large; + * return 5 : CSI abnormal: height too small; + * return 6 : CSI abnormal: height too large; + * return 7 : CSI abnormal: Unknown reason; + */ +uint8_t get_vi_state() +{ + char VI_State[10]={0}; + char cmd[100] = "cat /proc/cvitek/vi_dbg | grep -A 17 VIDevFPS | awk '{print $3}'"; + uint8_t FPS[2]; + uint8_t VIWHGTLSCnt[4]; + FILE* fp = popen( cmd, "r" ); + uint8_t ret = 0; + + if (fgets(VI_State, sizeof(VI_State), fp) != NULL){ + FPS[0] = atoi(VI_State); + // debug("VIDevFPS = %d\n", FPS[0]); + } else { + pclose(fp); + return ret; // VI not init; + } + if (fgets(VI_State, sizeof(VI_State), fp) != NULL){ + FPS[1] = atoi(VI_State); + // debug("VIFPS = %d\n", FPS[1]); + } + if (FPS[0] == 0){ + ret = 2; // HDMI not OK; + } else if (FPS[1] == 0){ + ret = 3; // HDMI OK ; CSI not; + } else { + ret = 1; // HDMI CSI OK; + } + if(ret == 3){ + // Ignore other information + uint8_t count = 0; + for(count = 0; count < 13; count ++){ + fgets(VI_State, sizeof(VI_State), fp); + } + // Check if the resolution might be set incorrectly + for(count = 0; count < 4; count ++){ + if (fgets(VI_State, sizeof(VI_State), fp) != NULL){ + // debug("VI_State = %s", VI_State); + VIWHGTLSCnt[count] = atoi(VI_State); + // printf("count = %d, val = %d\n", count, atoi(VI_State)); + } + } + + if(VIWHGTLSCnt[0] != 0) ret = 3; // The vi width setting value is too small + else if(VIWHGTLSCnt[1] != 0) ret = 4; // The vi width setting value is too large + else if(VIWHGTLSCnt[2] != 0) ret = 5; // The vi height setting value is too small + else if(VIWHGTLSCnt[3] != 0) ret = 6; // The vi height setting value is too large + else ret = 7; // printf("[kvmv] Unexpected situation\n"); + } + pclose(fp); + return ret; +} + +int get_hdmi_mode(void) +{ + if(access(hdmi_mode_path, F_OK) == 0){ + // exist + FILE *fp; + int file_size; + uint8_t tmp8; + uint8_t RW_Data[2]; + + fp = fopen(hdmi_mode_path, "r"); + fread(RW_Data, sizeof(char), 1, fp); + fclose(fp); + RW_Data[2] = 0; + tmp8 = atoi((char*)RW_Data); + if(tmp8 > 2) { + tmp8 = 0; + char Cmd[100]={0}; + sprintf(Cmd, "echo 0 > %s", hdmi_mode_path); + system(Cmd); + } + if(tmp8 != kvmv_cfg.hdmi_mode){ + kvmv_cfg.hdmi_mode = tmp8; + debug("[kvmv] hdmi mode = %d\n", kvmv_cfg.hdmi_mode); + return 1; + } else { + return 0; + } + } + kvmv_cfg.hdmi_mode = 0; + return 0; +} + +int get_manual_resolution(void) +{ + uint8_t RW_Data[35]; + FILE *fp; + int file_size; + uint16_t tmp_width, tmp_height; + int res = 0; + + // get res + if(access("/kvmapp/kvm/width", F_OK) == 0){ + fp = fopen("/kvmapp/kvm/width", "r"); + fseek(fp, 0, SEEK_END); + file_size = ftell(fp); + fseek(fp, 0, SEEK_SET); + fread(RW_Data, sizeof(char), file_size, fp); + fclose(fp); + RW_Data[file_size] = 0; + tmp_width = atoi((char*)RW_Data); + } else { + tmp_width = 1920; + } + if(access("/kvmapp/kvm/height", F_OK) == 0){ + fp = fopen("/kvmapp/kvm/height", "r"); + fseek(fp, 0, SEEK_END); + file_size = ftell(fp); + fseek(fp, 0, SEEK_SET); + fread(RW_Data, sizeof(char), file_size, fp); + fclose(fp); + RW_Data[file_size] = 0; + tmp_height = atoi((char*)RW_Data); + } else { + tmp_height = 1080; + } + + // res min limit + if(tmp_width < vi_min_width){ + tmp_width = vi_min_width; + char Cmd[100]={0}; + sprintf(Cmd, "echo %d > %s", vi_min_width, vi_width_path); + system(Cmd); + } + if(tmp_height < vi_min_height){ + tmp_height = vi_min_height; + char Cmd[100]={0}; + sprintf(Cmd, "echo %d > %s", vi_min_height, vi_height_path); + system(Cmd); + } + + // res change ? + if(kvmv_cfg.vi_width != tmp_width){ + kvmv_cfg.vi_width = tmp_width; + printf("[kvmk] get new width = %d\n", kvmv_cfg.vi_width); + res = 1; + } + if(kvmv_cfg.vi_height != tmp_height){ + kvmv_cfg.vi_height = tmp_height; + printf("[kvmk] get new height = %d\n", kvmv_cfg.vi_height); + res = 1; + } + return res; +} + +void lt6911_enable() +{ + uint8_t buf[2]; + buf[0] = 0xff; + buf[1] = 0x80; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); + + buf[0] = 0xee; + buf[1] = 0x01; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); + + if(kvmv_cfg.hdmi_version == 1){ + // disable watchdog + buf[0] = 0x10; + buf[1] = 0x00; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); + } +} + +void lt6911_disable() +{ + uint8_t buf[2]; + buf[0] = 0xff; + buf[1] = 0x80; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); + + buf[0] = 0xee; + buf[1] = 0x00; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); +} + +void lt6911_get_hdmi_errer() +{ + uint8_t buf[6]; + + buf[0] = 0xff; + buf[1] = 0xC0; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); + + buf[0] = 0x20; + buf[1] = 0x01; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); + + time::sleep_ms(100); + + buf[0] = 0x24; + LT6911_i2c.writeto(LT6911_ADDR, buf, 1); + + maix::Bytes *dat = LT6911_i2c.readfrom(LT6911_ADDR, 6); + + buf[0] = 0x20; + buf[1] = 0x07; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); + + for(int i = 0; i < 6; i++){ + buf[i] = (uint8_t)dat->data[i]; + } + delete dat; + + debug("hdmi_errer_code = %x, %x, %x, %x, %x, %x\n", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]); +} + +uint8_t lt6911_get_hdmi_res() +{ + uint8_t buf[2]; + uint8_t revbuf[4]; + uint16_t Vactive; + uint16_t Hactive; + + + if(kvmv_cfg.hdmi_version == 0){ + // LT6911C + buf[0] = 0xff; + buf[1] = 0xd2; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); + + buf[0] = 0x83; + buf[1] = 0x11; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); + + time::sleep_ms(5); + + // Vactive + buf[0] = 0x96; + LT6911_i2c.writeto(LT6911_ADDR, buf, 1); + maix::Bytes *dat0 = LT6911_i2c.readfrom(LT6911_ADDR, 2); + + // Hactive + buf[0] = 0x8b; + LT6911_i2c.writeto(LT6911_ADDR, buf, 1); + maix::Bytes *dat1 = LT6911_i2c.readfrom(LT6911_ADDR, 2); + + revbuf[0] = (uint8_t)dat0->data[0]; + revbuf[1] = (uint8_t)dat0->data[1]; + revbuf[2] = (uint8_t)dat1->data[0]; + revbuf[3] = (uint8_t)dat1->data[1]; + + Vactive = (revbuf[0] << 8)|revbuf[1]; + Hactive = (revbuf[2] << 8)|revbuf[3]; + Hactive *= 2; + + debug("[hdmi]HDMI res modification event\n"); + debug("[hdmi]new res: %d * %d\n", Hactive, Vactive); + + delete dat0; + delete dat1; + + if (Vactive != 0 && Hactive != 0){ + return 1; + } else { + // system("echo 0 > %s", vi_height_path); + // system("echo 0 > %s", vi_width_path); + return 0; + } + } else if (kvmv_cfg.hdmi_version == 1){ + // LT6911UXC + buf[0] = 0xff; + buf[1] = 0x86; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); + + buf[0] = 0xff; + buf[1] = 0x86; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); + + // HDMI signal disappear/stable + buf[0] = 0xA3; + LT6911_i2c.writeto(LT6911_ADDR, buf, 1); + maix::Bytes *dat0 = LT6911_i2c.readfrom(LT6911_ADDR, 1); + + revbuf[0] = (uint8_t)dat0->data[0]; + delete dat0; + + debug("[hdmi]HDMI-UXC res modification event\n"); + + if(revbuf[0] == 0x55) return 1; + else if(revbuf[0] == 0x88) return 0; + else return 0; + } else { + return 0; + } +} + +void lt6911_get_hdmi_clk() +{ + uint8_t buf[2]; + uint8_t revbuf[3]; + uint32_t clk; + + buf[0] = 0xff; + buf[1] = 0xa0; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); + + buf[0] = 0x34; + buf[1] = 0x0b; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); + + time::sleep_ms(50); + + // clk + buf[0] = 0xff; + buf[1] = 0xb8; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); + + buf[0] = 0xb1; + LT6911_i2c.writeto(LT6911_ADDR, buf, 1); + maix::Bytes *dat0 = LT6911_i2c.readfrom(LT6911_ADDR, 3); + + revbuf[0] = (uint8_t)dat0->data[0]; + revbuf[1] = (uint8_t)dat0->data[1]; + revbuf[2] = (uint8_t)dat0->data[2]; + revbuf[0] &= 0x07; + + clk = revbuf[0]; + clk <<= 8; + clk |= revbuf[1]; + clk <<= 8; + clk |= revbuf[2]; + + debug("[hdmi]HDMI CLK = %d\n", clk); + + delete dat0; +} + +uint8_t lt6911_get_csi_res(uint16_t *p_width, uint16_t *p_height) +{ + uint8_t ret = 0; + uint8_t buf[2]; + uint8_t revbuf[4]; + static uint16_t old_Vactive; + static uint16_t old_Hactive; + uint16_t Vactive; + uint16_t Hactive; + char Cmd[100]={0}; + + + if(kvmv_cfg.hdmi_version == 0){ + // LT6911C + buf[0] = 0xff; + buf[1] = 0xc2; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); + + // Vactive + buf[0] = 0x06; + LT6911_i2c.writeto(LT6911_ADDR, buf, 1); + maix::Bytes *dat0 = LT6911_i2c.readfrom(LT6911_ADDR, 2); + + // Hactive + buf[0] = 0x38; + LT6911_i2c.writeto(LT6911_ADDR, buf, 1); + maix::Bytes *dat1 = LT6911_i2c.readfrom(LT6911_ADDR, 2); + + revbuf[0] = (uint8_t)dat0->data[0]; + revbuf[1] = (uint8_t)dat0->data[1]; + revbuf[2] = (uint8_t)dat1->data[0]; + revbuf[3] = (uint8_t)dat1->data[1]; + + delete dat0; + delete dat1; + + Vactive = (revbuf[0] << 8)|revbuf[1]; + Hactive = (revbuf[2] << 8)|revbuf[3]; + } else if(kvmv_cfg.hdmi_version == 1) { + // LT6911UXC + debug("[hdmi]UXC get csi res\n"); + buf[0] = 0xff; + buf[1] = 0x85; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); + + // Vactive + buf[0] = 0xF0; + LT6911_i2c.writeto(LT6911_ADDR, buf, 1); + maix::Bytes *dat0 = LT6911_i2c.readfrom(LT6911_ADDR, 2); + + // Hactive + buf[0] = 0xEA; + LT6911_i2c.writeto(LT6911_ADDR, buf, 1); + maix::Bytes *dat1 = LT6911_i2c.readfrom(LT6911_ADDR, 2); + + revbuf[0] = (uint8_t)dat0->data[0]; + revbuf[1] = (uint8_t)dat0->data[1]; + revbuf[2] = (uint8_t)dat1->data[0]; + revbuf[3] = (uint8_t)dat1->data[1]; + + delete dat0; + delete dat1; + + Vactive = (revbuf[0] << 8)|revbuf[1]; + Hactive = (revbuf[2] << 8)|revbuf[3]; + } else { + return ret; + } + + if(old_Hactive != Hactive || old_Vactive != Vactive){ + old_Hactive = Hactive; + old_Vactive = Vactive; + // p_kvm_cfg->width = Hactive; + // p_kvm_cfg->height = Vactive; + *p_width = Hactive; + *p_height = Vactive; + ret = 1; + } + + debug("[hdmi]CSI res: %d * %d; new res = %d\n", Hactive, Vactive, ret); + // setenv("KVM_CSI_HEIGHT", to_string(Hactive).c_str(), 1); + // setenv("KVM_CSI_WIDTH", to_string(Vactive).c_str(), 1); + + sprintf(Cmd, "echo %d > %s", Hactive, vi_width_path); + system(Cmd); + sprintf(Cmd, "echo %d > %s", Vactive, vi_height_path); + system(Cmd); + system("sync"); + + + return ret; +} + +void lt6911_write_reg(uint8_t reg, uint8_t val) +{ + uint8_t buf[2]; + buf[0] = reg; + buf[1] = val; + LT6911_i2c.writeto(LT6911_ADDR, buf, 2); +} + +void lt6911_read_reg(uint8_t reg) +{ + uint8_t buf[16]; + buf[0] = reg; + LT6911_i2c.writeto(LT6911_ADDR, buf, 1); + + maix::Bytes *dat = LT6911_i2c.readfrom(LT6911_ADDR, 16); + + for(int i = 0; i < 16; i++){ + buf[i] = (uint8_t)dat->data[i]; + } + + delete dat; + + debug("[hdmi]%3x %3x %3x %3x %3x %3x %3x %3x |%3x %3x %3x %3x %3x %3x %3x %3x \n", \ + buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], \ + buf[8], buf[9], buf[10], buf[11], buf[12], buf[13], buf[14], buf[15]); +} + +uint8_t lt6911_read_one_reg(uint8_t reg) +{ + uint8_t ret; + ret = reg; + LT6911_i2c.writeto(LT6911_ADDR, &ret, 1); + + maix::Bytes *dat = LT6911_i2c.readfrom(LT6911_ADDR, 1); + + ret = (uint8_t)dat->data[0]; + + delete dat; + return ret; +} + +void lt6911_write_edid(void) +{ + uint8_t i, j; + uint8_t buf[2]; + lt6911_enable(); + + // to 90 + lt6911_write_reg(0xff, 0x90); + buf[0] = lt6911_read_one_reg(0x02); + buf[0] &= 0xDF; + lt6911_write_reg(0x02, buf[0]); + buf[0] |= 0x20; + lt6911_write_reg(0x02, buf[0]); + + // to 80 + lt6911_write_reg(0xff, 0x80); + // wren enable + lt6911_write_reg(0x5A, 0x86); + lt6911_write_reg(0x5A, 0x82); + + for(i = 0; i < 16; i++){ + // 写wren命令(为一个pulse,此时不需要考虑wrrd_mode,spi_paddr[1:0]的值) + lt6911_write_reg(0x5A, 0x86); + lt6911_write_reg(0x5A, 0x82); + + // 配置spi_len[3:0]= 15,可配置,spi内部加1,即配置一次写入16个字节 + lt6911_write_reg(0x5E, 0xEF); + lt6911_write_reg(0x5A, 0xA2); + lt6911_write_reg(0x5A, 0x82); + + lt6911_write_reg(0x58, 0x01); + + if(i < 8) { + for(j = 0; j < 16; j++){ + lt6911_write_reg(0x59, NanoKVM_edit[i*16+j]); + } + } else { + for(j = 0; j < 16; j++){ + lt6911_write_reg(0x59, 0x00); + } + } + + // 把fifo数据写到flash(当wrrd_mode = 1,spi_paddr= 2’b10,addr[23:0](地址在写入过程中需要保持不变)准备好的情况下,给一个spi_sta的pulse,就开始写入flash) + lt6911_write_reg(0x5B, 0x00); + lt6911_write_reg(0x5C, 0x21); + lt6911_write_reg(0x5D, i*16); + lt6911_write_reg(0x5E, 0xE0); + lt6911_write_reg(0x5A, 0x92); + lt6911_write_reg(0x5A, 0x82); + + } + + lt6911_write_reg(0x5A, 0x8A); + lt6911_write_reg(0x5A, 0x82); + + lt6911_disable(); + debug("[hdmi]lt6911_write_edid OK\n"); +} + +void lt6911_read_edid(void) +{ + uint8_t i, j; + lt6911_enable(); + + // to 80 + lt6911_write_reg(0xff, 0x80); + lt6911_write_reg(0xEE, 0x01); + + // configure parameter + lt6911_write_reg(0x5A, 0x80); + lt6911_write_reg(0x5E, 0xC0); + lt6911_write_reg(0x58, 0x00); + lt6911_write_reg(0x59, 0x51); + lt6911_write_reg(0x5A, 0x90); + time::sleep_ms(1); + lt6911_write_reg(0x5A, 0x80); + + for(i = 0; i < 16; i++){ + // to 90 + lt6911_write_reg(0xff, 0x90); + // fifo rst_n + lt6911_write_reg(0x02, 0xdf); + time::sleep_ms(1); + lt6911_write_reg(0x02, 0xff); + + // wren + // to 80 + lt6911_write_reg(0xff, 0x80); + // fifo rst_n + lt6911_write_reg(0x5A, 0x84); + time::sleep_ms(1); + lt6911_write_reg(0x5A, 0x80); + + // flash to fifo + lt6911_write_reg(0x5E, 0x6F); + lt6911_write_reg(0x5A, 0xA0); + time::sleep_ms(1); + lt6911_write_reg(0x5A, 0x80); + lt6911_write_reg(0x5B, 0x00); + lt6911_write_reg(0x5C, 0x21); + lt6911_write_reg(0x5D, 16*i); + lt6911_write_reg(0x5A, 0x90); + time::sleep_ms(5); + lt6911_write_reg(0x5A, 0x80); + lt6911_write_reg(0x58, 0x01); + + // for(j = 0; j < 16; j++){ + debug("[hdmi] EDID: "); + lt6911_read_reg(0x5F); + // } + time::sleep_ms(10); + } + + // wrdi + lt6911_write_reg(0x5A, 0x88); + time::sleep_ms(1); + lt6911_write_reg(0x5A, 0x80); + + lt6911_disable(); + debug("[hdmi]lt6911_read_edid OK\n"); +} + +void lt6911_read_fw(void) +{ + uint32_t i, j; + uint8_t buf[3]; + lt6911_enable(); + + // to 80 + lt6911_write_reg(0xff, 0x80); + lt6911_write_reg(0xEE, 0x01); + + // configure parameter + lt6911_write_reg(0x5A, 0x80); + lt6911_write_reg(0x5E, 0xC0); + lt6911_write_reg(0x58, 0x00); + lt6911_write_reg(0x59, 0x51); + lt6911_write_reg(0x5A, 0x90); + time::sleep_ms(1); + lt6911_write_reg(0x5A, 0x80); + + for(i = 0x1; i < 50000; i++){ + buf[0] = ((i*16) & 0xFF0000) >> 16; + buf[1] = ((i*16) & 0xFF00) >> 8; + buf[2] = ((i*16) & 0xFF); + // to 90 + lt6911_write_reg(0xff, 0x90); + // fifo rst_n + lt6911_write_reg(0x02, 0xdf); + time::sleep_ms(1); + lt6911_write_reg(0x02, 0xff); + + // wren + // to 80 + lt6911_write_reg(0xff, 0x80); + // fifo rst_n + lt6911_write_reg(0x5A, 0x84); + time::sleep_ms(1); + lt6911_write_reg(0x5A, 0x80); + + // flash to fifo + lt6911_write_reg(0x5E, 0x6F); + lt6911_write_reg(0x5A, 0xA0); + time::sleep_ms(1); + lt6911_write_reg(0x5A, 0x80); + lt6911_write_reg(0x5B, buf[0]); + lt6911_write_reg(0x5C, buf[1]); + lt6911_write_reg(0x5D, buf[2]); + lt6911_write_reg(0x5A, 0x90); + time::sleep_ms(5); + lt6911_write_reg(0x5A, 0x80); + lt6911_write_reg(0x58, 0x01); + + // for(j = 0; j < 16; j++){ + debug("[hdmi]REG %6x: ", i*16); + lt6911_read_reg(0x5F); + // } + time::sleep_ms(10); + } + + // wrdi + lt6911_write_reg(0x5A, 0x88); + time::sleep_ms(1); + lt6911_write_reg(0x5A, 0x80); + + lt6911_disable(); + debug("[hdmi]lt6911_read_edid OK\n"); +} + +// ============================================================== + +void* vi_subsystem_detection(void * arg) +{ + uint64_t __attribute__((unused)) int_time; + + FILE *fp; + uint8_t RW_Data[2]; + uint8_t file_size; + uint8_t tmp8; + uint8_t rising_times = 0; + uint8_t falling_times = 0; + uint8_t cam_need_restart = 0; + kvmv_cfg.thread_is_running = 1; + if(access("/proc/lt_int", F_OK) != 0){ + time::sleep_ms(10); + debug("[hdmi]/proc/lt_int not ok\n"); + } + + if(access("/etc/kvm/hdmi_version", F_OK) == 0){ + fp = fopen("/etc/kvm/hdmi_version", "r"); + fread(RW_Data, sizeof(char), 1, fp); + fclose(fp); + if(RW_Data[0] == 'u'){ + // 6911uxc + kvmv_cfg.hdmi_version = 1; + debug("[hdmi]HDMI-UXC exist!\n"); + } else { + // 6911c + kvmv_cfg.hdmi_version = 0; + debug("[hdmi]HDMI-C exist!\n"); + } + RW_Data[0] = 0; + } else { + kvmv_cfg.hdmi_version = 0; + } + + // while(!app::need_exit()) + uint8_t while_count_get_hdmi_mode = 0; + uint8_t while_count_detect_res = 0; + uint8_t auto_trying_times = 0; + while(true) + { + uint8_t get_new_hdmi_mode = 0; + if(kvmv_cfg.try_exit_thread == 1) + break; + + // Low-frequency detection mode change + if (while_count_get_hdmi_mode >= 100){ + while_count_get_hdmi_mode = 0; + get_new_hdmi_mode = get_hdmi_mode(); + } else { + while_count_get_hdmi_mode ++; + } + + switch (kvmv_cfg.hdmi_mode){ + case 0: + // Switching to Mode 0 requires restarting HDMI (effective only for PCIe version) + // Handling of automatic detection situations + if(get_new_hdmi_mode == 1){ + kvmv_cfg.vi_detect_state = 0; + // reset hdmi_state + char Cmd[100]={0}; + sprintf(Cmd, "echo 0 > %s", hdmi_state_path); + system(Cmd); + // reset hdmi + kvmv_hdmi_control(0); + time::sleep_ms(10); + kvmv_hdmi_control(1); + } + // Initialize VI with I2C information after HDMI insertion + fp = fopen(hdmi_state_path, "r+"); + if(fp != NULL){ + // fseek(fp, 0, SEEK_END); + // file_size = ftell(fp); + // fseek(fp, 0, SEEK_SET); + fread(RW_Data, sizeof(char), 2, fp); + tmp8 = atoi((char*)RW_Data); + // debug("[hdmi]UXC tmp8 = %d\n", tmp8); + if(tmp8 != 0){ + // reset hdmi_state + fputs("0", fp); + // count edge ints + rising_times = tmp8%10; // RISING times + falling_times = tmp8/10; // FALLING times + + if(kvmv_cfg.hdmi_stop_flag != 1){ + kvmv_cfg.hdmi_reading_flag = 1; + if(kvmv_cfg.hdmi_version == 0){ + // LT6911C + if(rising_times != 0){ + lt6911_enable(); + if(lt6911_get_hdmi_res()){ + // hdmi get res + debug("[hdmi] C HDMI cable insertion!\n"); + kvmv_cfg.hdmi_cable_state = 1; + kvmv_cfg.reopen_cam_flag = lt6911_get_csi_res(&kvmv_cfg.vi_width, &kvmv_cfg.vi_height); + } else { + // HDMI res = 0*0/x*0 + debug("[hdmi] C HDMI cable unplugged!\n"); + kvmv_cfg.hdmi_cable_state = 0; + } + lt6911_disable(); + } + } else if (kvmv_cfg.hdmi_version == 1){ + // LT6911UXC + debug("[hdmi] UXC int\n"); + if(falling_times != 0){ + debug("[hdmi] UXC int && \n"); + lt6911_enable(); + if(lt6911_get_hdmi_res()){ + // hdmi get res + debug("[hdmi] UXC HDMI cable insertion!\n"); + kvmv_cfg.hdmi_cable_state = 1; + kvmv_cfg.reopen_cam_flag = lt6911_get_csi_res(&kvmv_cfg.vi_width, &kvmv_cfg.vi_height); + } else { + // HDMI res = 0*0/x*0 + debug("[hdmi] UXC HDMI cable unplugged!\n"); + kvmv_cfg.hdmi_cable_state = 0; + } + lt6911_disable(); + } + } + kvmv_cfg.hdmi_reading_flag = 0; + } + } + fclose(fp); + } + break; + /* Mode 1 & 2 will disable API access to the image during detection, + and will output -4: Modifying image resolution, please wait.*/ + case 1: // Automatically trying common resolutions + /* kvmv_cfg.vi_detect_state : + * 0: HDMI standard mode, detection program does not interfere with the camera + * 1: Preparing / Testing in progress + * 2: Test completed: Suitable resolution found, + */ + if(get_new_hdmi_mode == 1){ + kvmv_cfg.vi_detect_state = 1; + auto_trying_times = 0; + } + if(kvmv_cfg.vi_detect_state == 1){ + uint8_t err_code = get_vi_state(); + debug("[kvmv] get_vi_state() = %d\n", err_code); + switch(err_code){ + case 0: + // shouldn't be possible to run here + printf("[kvmv] VI not init\n"); + break; + case 1: + printf("[kvmv] VI subsystem is normal\n"); + kvmv_cfg.vi_detect_state = 2; + break; + case 2: + // HDMI not detected or resolution not supported; interval checks will continue + printf("[kvmv] Cannot obtain HDMI input\n"); + break; + case 3: // width too small + case 4: // width too large + case 5: // height too small + case 6: // height too large + // CSI abnormal due to resolution error + // The test list is short; sequential testing can be performed + if(auto_trying_times < sizeof(hdmi_res_list)/4){ + char Cmd[100]={0}; + printf("[kvmv] Trying %d * %d res ..\n", hdmi_res_list[auto_trying_times][0], hdmi_res_list[auto_trying_times][1]); + sprintf(Cmd, "echo %d > %s", hdmi_res_list[auto_trying_times][0], vi_width_path); + system(Cmd); + sprintf(Cmd, "echo %d > %s", hdmi_res_list[auto_trying_times][1], vi_height_path); + system(Cmd); + printf("[kvmv] restart cam...\n"); + cam->restart(default_vpss_width, default_vpss_height, image::FMT_YVU420SP); + auto_trying_times++; + time::sleep_ms(10); + } else { + printf("[kvmv] Suitable resolution not found, switching to manual input mode automatically\n"); + kvmv_cfg.vi_detect_state = 1; + char Cmd[100]={0}; + sprintf(Cmd, "echo 2 > %s", hdmi_mode_path); + system(Cmd); + while_count_get_hdmi_mode = 100; // Take effect immediately + } + break; + case 7: // Unknown reason + printf("[kvmv] CSI abnormal: Unknown reason\n"); + break; + } + } else if (kvmv_cfg.vi_detect_state == 2){ + // Low-frequency detection of HDMI status, no log output + uint8_t err_code = get_vi_state(); + if (err_code != 1) { + kvmv_cfg.vi_detect_state = 1; + auto_trying_times = 0; + } + time::sleep_ms(1000); + } else { + kvmv_cfg.vi_detect_state = 1; + } + break; + case 2: + // Manually initialize VI. + if (while_count_detect_res >= 100){ + while_count_detect_res = 0; + if (kvmv_cfg.vi_detect_state == 1){ + // detect_res + if (get_manual_resolution()) { + printf("[kvmv] restart cam...\n"); + cam->restart(default_vpss_width, default_vpss_height, image::FMT_YVU420SP); + } + + // dbg info + uint8_t err_code = get_vi_state(); + switch(err_code){ + case 0: + debug("[kvmv] VI not init\n"); + break; + case 1: + debug("[kvmv] HDMI and CSI status are normal\n"); + kvmv_cfg.vi_detect_state = 2; + break; + case 2: + debug("[kvmv] HDMI abnormal\n"); + break; + case 3: + debug("[kvmv] CSI abnormal: width too small\n"); + break; + case 4: + debug("[kvmv] CSI abnormal: width too large\n"); + break; + case 5: + debug("[kvmv] CSI abnormal: height too small\n"); + break; + case 6: + debug("[kvmv] CSI abnormal: height too large\n"); + break; + case 7: + debug("[kvmv] CSI abnormal: Unknown reason\n"); + break; + } + } else if (kvmv_cfg.vi_detect_state == 2){ + // detection of HDMI status, no log output + uint8_t err_code = get_vi_state(); + if (err_code != 1) kvmv_cfg.vi_detect_state = 1; + } else { + kvmv_cfg.vi_detect_state = 1; + } + + } else { + while_count_detect_res ++; + } + break; + + default: + debug("Non-existent hdmi state = %d\n", kvmv_cfg.hdmi_mode); + break; + } + + time::sleep_ms(10); + } + kvmv_cfg.thread_is_running = 0; +} + +int sync_vi_res() +{ + int res = 0; + uint8_t RW_Data[35]; + FILE *fp; + int file_size; + uint16_t tmp16; + + // vi_width: + if (access(vi_width_path, F_OK) != 0){ + kvmv_cfg.vi_width = default_vi_width; + kvmv_cfg.vi_height = default_vi_height; + res = -1; + return res; + } else { + fp = fopen(vi_width_path, "r"); + fseek(fp, 0, SEEK_END); + file_size = ftell(fp); + fseek(fp, 0, SEEK_SET); + fread(RW_Data, sizeof(char), file_size, fp); + fclose(fp); + RW_Data[file_size] = 0; + tmp16 = atoi((char*)RW_Data); + if(tmp16 != kvmv_cfg.vi_width){ + kvmv_cfg.vi_width = tmp16; + debug("[hdmi] Get new HDMI width = %d\r\n", kvmv_cfg.vi_width); + res = 1; + } + } + // vi_height: + if (access(vi_height_path, F_OK) != 0){ + kvmv_cfg.vi_height = default_vi_height; + res = -1; + return res; + } else { + fp = fopen(vi_height_path, "r"); + fseek(fp, 0, SEEK_END); + file_size = ftell(fp); + fseek(fp, 0, SEEK_SET); + fread(RW_Data, sizeof(char), file_size, fp); + fclose(fp); + RW_Data[file_size] = 0; + tmp16 = atoi((char*)RW_Data); + if(tmp16 != kvmv_cfg.vi_height){ + kvmv_cfg.vi_height = tmp16; + debug("[hdmi] Get new HDMI height = %d\r\n", kvmv_cfg.vi_height); + res = 1; + } + } + return res; +} + +void jpg_dump(kvmv_data_t* dump_to, image::Image *raw) +{ + dump_to->p_img_data = (uint8_t *)malloc(raw->data_size()); + dump_to->img_data_size = raw->data_size(); + dump_to->img_data_type = VENC_MJPEG; + memcpy(dump_to->p_img_data, (uint8_t *)raw->data(), raw->data_size()); +} + +uint8_t kvmvenc_gop = default_h264_gop; +kvm_venc_t kvm_venc; +mmf_venc_cfg_t cfg; +void init_venc_h264(uint16_t _width, uint16_t _height, uint16_t _qlty) +{ + cfg.type = 2; //1, h265, 2, h264 + cfg.w = _width; + cfg.h = _height; + cfg.fmt = mmf_invert_format_to_mmf(image::Format::FMT_YVU420SP); + cfg.jpg_quality = 0; // unused + cfg.gop = kvmvenc_gop; + cfg.intput_fps = 60; + cfg.output_fps = 60; + cfg.bitrate = _qlty; // 码率 + + kvm_venc.mmf_venc_chn = default_venc_chn; + kvm_venc.enc_h264_init = 0; + kvm_venc.kvm_venc_cfg = cfg; + + // if(mmf_vdec_is_used(kvm_venc.mmf_venc_chn)){ + mmf_del_venc_channel(kvm_venc.mmf_venc_chn); + // } + if (0 != mmf_add_venc_channel(kvm_venc.mmf_venc_chn, &kvm_venc.kvm_venc_cfg)) { + err::check_raise(err::ERR_RUNTIME, "mmf venc init failed!"); + } + kvm_venc.enc_h264_init = 1; + + // init_kvm_h264_stream(&kvm_h264_stream, mmf_stream_buf); + // init_h264_stream_struct(&kvm_h264_stream); +} + +int h264_stream_dump(kvmv_data_t* dump_to, mmf_stream_t* dump_from) +{ + static int8_t I_Frame_index = -1; + // debug("[kvmv]dump_from->count = %d\n", dump_from->count); + if (dump_from->count == 3) { + // debug("[kvmv]dump I-Frame\r\n"); + if(kvmv_SPS_buffer.p_img_data != NULL || kvmv_PPS_buffer.p_img_data != NULL) + return IMG_BUFFER_FULL; + + kvmv_SPS_buffer.p_img_data = (uint8_t *)malloc(dump_from->data_size[0]); + kvmv_PPS_buffer.p_img_data = (uint8_t *)malloc(dump_from->data_size[1]); + dump_to->p_img_data = (uint8_t *)malloc(dump_from->data_size[2]); + + kvmv_SPS_buffer.img_data_size = dump_from->data_size[0]; + kvmv_PPS_buffer.img_data_size = dump_from->data_size[1]; + dump_to->img_data_size = dump_from->data_size[2]; + + kvmv_SPS_buffer.img_data_type = IMG_H264_TYPE_SPS; + kvmv_PPS_buffer.img_data_type = IMG_H264_TYPE_PPS; + dump_to->img_data_type = IMG_H264_TYPE_IF; + + memcpy(kvmv_SPS_buffer.p_img_data, dump_from->data[0], dump_from->data_size[0]); + memcpy(kvmv_PPS_buffer.p_img_data, dump_from->data[1], dump_from->data_size[1]); + memcpy(dump_to->p_img_data, dump_from->data[2], dump_from->data_size[2]); + + debug("[kvmv]SPS size = %d\n", kvmv_SPS_buffer.img_data_size); + debug("[kvmv]PPS size = %d\n", kvmv_PPS_buffer.img_data_size); + debug("[kvmv]I-Frame size = %d\n", dump_to->img_data_size); + + return IMG_H264_TYPE_IF; + + } else if (dump_from->count == 1) { + // debug("[kvmv]dump P-Frame\r\n"); + I_Frame_index = -1; + dump_to->p_img_data = (uint8_t *)malloc(dump_from->data_size[0]); + dump_to->img_data_size = dump_from->data_size[0]; + dump_to->img_data_type = IMG_H264_TYPE_PF; + memcpy(dump_to->p_img_data, dump_from->data[0], dump_from->data_size[0]); + return IMG_H264_TYPE_PF; + } else { + debug("[kvmv]venc error!\r\n"); + return IMG_VENC_ERROR; + } +} + +void set_h264_gop(uint8_t _gop) +{ + kvm_venc.enc_h264_init = 0; // call + kvmvenc_gop = maxmin_data(100, 10, (int)_gop); +} + +// uint8_t get_h264_gop(void) +// { +// return kvm_venc.kvm_venc_cfg.gop; +// } + +// uint8_t get_hdmi_width(void) +// { +// return kvm_venc.kvm_venc_cfg.gop; +// } + +int8_t raw_to_h264(image::Image *raw, kvmv_data_t* ret_stream, uint16_t _qlty) +{ + uint64_t __attribute__((unused)) start_time; + uint64_t __attribute__((unused)) frame_time; + int8_t ret = 0; + static uint8_t P_Frame_Count = 0; + // start_time = time::time_ms(); + // log::info("getimg: %d \r\n", (int)(time::time_ms())); + mmf_stream_t _stream = {0}; + if(kvm_venc.enc_h264_init != 1 || raw->width() != kvm_venc.kvm_venc_cfg.w || raw->height() != kvm_venc.kvm_venc_cfg.h || _qlty != kvm_venc.kvm_venc_cfg.bitrate){ + debug("[kvmv]init_venc_h264 enc_h264_init = %d; raw->width() = %d | %d raw->height() = %d | %d \n", + kvm_venc.enc_h264_init, + raw->width(), kvm_venc.kvm_venc_cfg.w, + raw->height(), kvm_venc.kvm_venc_cfg.h); + + init_venc_h264(raw->width(), raw->height(), _qlty); + debug("[kvmv]init_venc_h264 finish enc_h264_init = %d; raw->width() = %d | %d raw->height() = %d | %d \n", + kvm_venc.enc_h264_init, + raw->width(), kvm_venc.kvm_venc_cfg.w, + raw->height(), kvm_venc.kvm_venc_cfg.h); + // if(kvm_venc.enc_h264_init == 1){ + // init_venc_h264(raw->width(), raw->height(), _qlty); + // } else { + // init_venc_h264(default_vpss_width, default_vpss_height, default_h264_qlty); + // } + } + // log::info("init(): %d \r\n", (int)(time::time_ms() - start_time)); + if (mmf_venc_push(kvm_venc.mmf_venc_chn, (uint8_t *)raw->data(), raw->width(), raw->height(), mmf_invert_format_to_mmf(raw->format()))) { + mmf_del_venc_channel(kvm_venc.mmf_venc_chn); + kvm_venc.enc_h264_init = 0; + // rtmp->unlock(); + debug("[kvmv]mmf venc push failed!\n"); + // err::check_raise(err::ERR_RUNTIME, "mmf venc push failed!\r\n"); + return -1; + } + // log::info("push(): %d \r\n", (int)(time::time_ms() - start_time)); + if (mmf_venc_pop(kvm_venc.mmf_venc_chn, &_stream)) { + // log::error("mmf_venc_pop failed\n"); + mmf_venc_free(kvm_venc.mmf_venc_chn); + mmf_del_venc_channel(kvm_venc.mmf_venc_chn); + kvm_venc.enc_h264_init = 0; + debug("[kvmv]mmf venc push failed!\n"); + // rtmp->unlock(); + return -1; + } + // log::info("pop(): %d \r\n", (int)(time::time_ms() - start_time)); + ret = h264_stream_dump(ret_stream, &_stream); + mmf_venc_free(kvm_venc.mmf_venc_chn); + // log::info("dump(): %d \r\n", (int)(time::time_ms() - start_time)); + + // debug("[kvmv]_stream.data[0][4] = %d;\n", _stream.data[0][4]); + debug("[kvmv]Frame size = %d;\n", ret_stream->img_data_size); + + if(ret == IMG_H264_TYPE_IF){ + debug("[kvmv]================ GOP = %d ================\n", kvm_venc.kvm_venc_cfg.gop); + debug("[kvmv]SPS; PPS; I-Frame, I-Frame size = %d\n", ret_stream->img_data_size); + P_Frame_Count = 0; + + } else if(ret == IMG_H264_TYPE_PF){ + debug("[kvmv]P-Frame size = %d, P-count = %d\n", ret_stream->img_data_size, P_Frame_Count); + P_Frame_Count++; + } + debug("[kvmv]dump ret = %d\n", ret); + return ret; +} + +void kvmv_init(uint8_t _debug_info_en) +{ + pthread_t thread; + if(_debug_info_en == 0) debug_en = 0; + else debug_en = 1; + + // debug("[kvmv]kvmv_init - 1\r\n"); + + cam->hmirror(1); + cam->vflip(1); + cam->restart(default_vpss_width, default_vpss_height, image::FMT_YVU420SP); + for(int i = 0; i < kvmv_data_buffer_size; i++){ + kvmv_data_buffer[i].p_img_data = NULL; + } + kvmv_SPS_buffer.p_img_data = NULL; + kvmv_PPS_buffer.p_img_data = NULL; + + kvmv_cfg.try_exit_thread = 0; + // debug("[kvmv]kvmv_init - 2\r\n"); + + if(kvmv_cfg.thread_is_running == 1){ + debug("[kvmv]thread is running!\r\n"); + } else { + if (0 != pthread_create(&thread, NULL, vi_subsystem_detection, NULL)) { + debug("[kvmv]create thread failed!\r\n"); + // return -1; + } + } + // debug("[kvmv]kvmv_init - 3\r\n"); +} + +uint8_t check_kvmv(uint8_t _try_num) +{ + if(kvmv_cfg.hdmi_cable_state == 0){ + debug("[kvmv]HDMI Cable not exist!\n"); + return 0; + } + if(_try_num >= KVMV_MAX_TRY_NUM){ + debug("[kvmv]try_num >= KVMV_MAX_TRY_NUM!\n"); + return 0; + } + // if(sync_vi_res() != 0){ + if(kvmv_cfg.reopen_cam_flag == 1){ + // vi size changed + kvmv_cfg.reopen_cam_flag = 0; + // cam->open(kvmv_cfg.vpss_width, kvmv_cfg.vpss_height, image::FMT_YVU420SP, 3); + cam->restart(default_vpss_width, default_vpss_height, image::FMT_YVU420SP); + debug("[kvmv]vi size changed, try again\n"); + return 1; + } + debug("[kvmv]just try again\n"); + return 1; +} + + +/********************************************************************************** + * @name kvmv_read_img + * @author Sipeed BuGu + * @date 2024/10/25 + * @version R1.0 + * @brief Acquire the encoded image with auto init + * @param _width @input: Output image width + * @param _height @input: Output image height + * @param _type @input: Encode type + * @param _qlty @input: MJPEG: (50-100) | H264: (500-10000) + * @param _pp_kvm_data @output: Encode data + * @param _p_kvmv_data_size @output: Encode data size + * @return + -4: Modifying image resolution, please wait + -3: img buffer full + -2: VENC Errorl + -1: No images were acquired + 0: Acquire MJPEG encoded images + 1: Acquire H264 encoded images(SPS) + 2: Acquire H264 encoded images(PPS) + 3: Acquire H264 encoded images(I) + 4: Acquire H264 encoded images(P) + **********************************************************************************/ +int kvmv_read_img(uint16_t _width, uint16_t _height, uint8_t _type, uint16_t _qlty, uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size) +{ + // uint64_t __attribute__((unused)) start_time = time::time_ms(); + if (kvmv_cfg.vi_detect_state == 1){ + return -4; + } + uint8_t try_num = 0; + do { + if(kvmv_cfg.vpss_width != _width || kvmv_cfg.vpss_height != _height){ + if(_width == 0 || _height == 0){ + // Follow the HDMI output + kvmv_cfg.vpss_width = kvmv_cfg.vi_width; + kvmv_cfg.vpss_height = kvmv_cfg.vi_height; + + if(kvmv_cfg.Auto_res == 0){ + kvmv_cfg.Auto_res = 1; + cam->set_resolution(kvmv_cfg.vpss_width, kvmv_cfg.vpss_height); + kvmv_cfg.reinit_flag = 1; + } + + } else { + kvmv_cfg.Auto_res = 0; + // Set the output + kvmv_cfg.vpss_width = _width; + kvmv_cfg.vpss_height = _height; + + cam->set_resolution(kvmv_cfg.vpss_width, kvmv_cfg.vpss_height); + kvmv_cfg.reinit_flag = 1; + } + } + + // + if (kvmv_cfg.reinit_flag == 1) { + cam->hmirror(1); + cam->vflip(1); + kvmv_cfg.reinit_flag = 0; + } + // debug("[kvmv]befor read img: %d \r\n", (int)(time::time_ms() - start_time)); + image::Image *img = cam->read(); + // debug("[kvmv]read img: %d \r\n", (int)(time::time_ms() - start_time)); + if(img != NULL){ + if(kvmv_cfg.cam_state == 0) { + kvmv_cfg.cam_state = 1; + kvmv_cfg.hdmi_cable_state = 1; + system("echo 1 > /kvmapp/kvm/state"); + } + } else { + if(kvmv_cfg.cam_state == 1) { + kvmv_cfg.cam_state = 0; + system("echo 0 > /kvmapp/kvm/state"); + } + delete img; + debug("[kvmv]can`t get img...\n"); + continue; + } + // debug("[kvmv]cheak img null?: %d \r\n", (int)(time::time_ms() - start_time)); + + // img exist + // Encode + if(_type == VENC_H264 && kvmv_cfg.venc_type != _type){ + kvm_venc.enc_h264_init = 1; + // debug("[kvmv] change to h264\n"); + } + kvmv_cfg.venc_type = _type; + + if(kvmv_cfg.venc_type == VENC_MJPEG){ + image::Image *jpg = img->to_jpeg(maxmin_data(99, 51, (int)_qlty)); + kvmv_data_t* p_kvmv_data = get_save_buffer(); + if(p_kvmv_data == NULL){ + // buffer full + delete jpg; + delete img; + debug("[kvmv]jpg buffer full\n"); + *_pp_kvm_data = NULL; + return IMG_BUFFER_FULL; + } + jpg_dump(p_kvmv_data, jpg); + delete jpg; + delete img; + *_pp_kvm_data = p_kvmv_data->p_img_data; + *_p_kvmv_data_size = p_kvmv_data->img_data_size; + return IMG_MJPEG_TYPE; + } else if (kvmv_cfg.venc_type == VENC_H264){ + int ret; + kvmv_data_t* p_kvmv_data = get_save_buffer(); + if(p_kvmv_data == NULL){ + // buffer full + delete img; + *_pp_kvm_data = NULL; + return IMG_BUFFER_FULL; + } + // debug("[kvmv]get_save_buffer: %d \r\n", (int)(time::time_ms() - start_time)); + ret = raw_to_h264(img, p_kvmv_data, maxmin_data(10000, 500, (int)_qlty)); + // debug("[kvmv]venc raw_to_h264: %d \r\n", (int)(time::time_ms() - start_time)); + delete img; + *_pp_kvm_data = p_kvmv_data->p_img_data; + *_p_kvmv_data_size = p_kvmv_data->img_data_size; + return ret; + } + } while (check_kvmv(try_num++)); + // debug("[kvmv]return: %d \r\n", (int)(time::time_ms() - start_time)); + *_pp_kvm_data = NULL; + return IMG_NOT_EXIST; +} + +int kvmv_get_sps_frame(uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size) +{ + if(kvmv_SPS_buffer.p_img_data == NULL){ + return IMG_NOT_EXIST; + } else { + *_pp_kvm_data = kvmv_SPS_buffer.p_img_data; + *_p_kvmv_data_size = kvmv_SPS_buffer.img_data_size; + return IMG_H264_TYPE_SPS; + } + return IMG_NOT_EXIST; +} + +int kvmv_get_pps_frame(uint8_t** _pp_kvm_data, uint32_t* _p_kvmv_data_size) +{ + if(kvmv_PPS_buffer.p_img_data == NULL){ + return IMG_NOT_EXIST; + } else { + *_pp_kvm_data = kvmv_PPS_buffer.p_img_data; + *_p_kvmv_data_size = kvmv_PPS_buffer.img_data_size; + return IMG_H264_TYPE_PPS; + } + return IMG_NOT_EXIST; +} + +int free_kvmv_data(uint8_t ** _pp_kvm_data) +{ + // debug("[kvmv]free_kvmv_data - 1\r\n"); + for(int i = 0; i < kvmv_data_buffer_size; i++){ + if(*_pp_kvm_data == kvmv_data_buffer[i].p_img_data){ + // debug("[kvmv]free buffer : %d\n", *_pp_kvm_data); + if (*_pp_kvm_data != NULL){ + // debug("[kvmv]free_kvmv_data - 2\r\n"); + free(*_pp_kvm_data); + // debug("[kvmv]free_kvmv_data - 3\r\n"); + kvmv_data_buffer[i].p_img_data = NULL; + uint8_t _type = kvmv_data_buffer[i].img_data_type; + if(_type == IMG_H264_TYPE_IF){ + free(kvmv_SPS_buffer.p_img_data); + free(kvmv_PPS_buffer.p_img_data); + kvmv_SPS_buffer.p_img_data = NULL; + kvmv_PPS_buffer.p_img_data = NULL; + } + return _type; + } else { + return IMG_NOT_EXIST; + } + } + } + return IMG_NOT_EXIST; +} + +void free_all_kvmv_data() +{ + for(int i = 0; i <= kvmv_data_buffer_size; i++){ + if(kvmv_data_buffer[i].p_img_data != NULL){ + free(kvmv_data_buffer[i].p_img_data); + kvmv_data_buffer[i].p_img_data = NULL; + } + } + if(kvmv_SPS_buffer.p_img_data != NULL) free(kvmv_SPS_buffer.p_img_data); + if(kvmv_PPS_buffer.p_img_data != NULL) free(kvmv_PPS_buffer.p_img_data); + kvmv_SPS_buffer.p_img_data = NULL; + kvmv_PPS_buffer.p_img_data = NULL; +} + +void kvmv_deinit() +{ + kvmv_cfg.try_exit_thread = 1; + cam->close(); + mmf_deinit(); + free_all_kvmv_data(); +} + +uint8_t kvmv_hdmi_control(uint8_t _en) +{ + if(kvmv_cfg.hw_version == 0){ + + FILE *fp; + uint8_t RW_Data[2]; + if(access("/etc/kvm/hw", F_OK) == 0){ + fp = fopen("/etc/kvm/hw", "r"); + fread(RW_Data, sizeof(char), 1, fp); + fclose(fp); + switch(RW_Data[0]){ + case 'a': + case 'b': + case 'p': + kvmv_cfg.hw_version = RW_Data[0]; + break; + default : + kvmv_cfg.hw_version = 'a'; + } + } + } + if(kvmv_cfg.hw_version != 'p'){ + debug("[kvmv]Hardware not support!\n"); + return -1; + } + if(access("/sys/class/gpio/gpio451/value", F_OK) != 0){ + system("echo 451 > /sys/class/gpio/export"); + system("echo out > /sys/class/gpio/gpio451/direction"); + } + if(_en == 0){ + kvmv_cfg.hdmi_stop_flag = 1; + while(kvmv_cfg.hdmi_reading_flag == 1) time::sleep_ms(10); + system("echo 0 > /sys/class/gpio/gpio451/value"); + return 0; + } else { + kvmv_cfg.hdmi_stop_flag = 0; + system("echo 1 > /sys/class/gpio/gpio451/value"); + return 0; + } + return -1; +} diff --git a/vision/components/kvm_mmf/CMakeLists.txt b/vision/components/kvm_mmf/CMakeLists.txt new file mode 100644 index 0000000..a0e5813 --- /dev/null +++ b/vision/components/kvm_mmf/CMakeLists.txt @@ -0,0 +1,200 @@ + +list(APPEND ADD_REQUIREMENTS basic ini) +list(APPEND ADD_INCLUDE "include") + +# sophgo-middleware +set(middleware_version_str "${CONFIG_SOPHGO_MIDDLEWARE_VERSION_MAJOR}.${CONFIG_SOPHGO_MIDDLEWARE_VERSION_MINOR}.${CONFIG_SOPHGO_MIDDLEWARE_VERSION_PATCH}") +set(middleware_unzip_path "${DL_EXTRACTED_PATH}/sophgo-middleware") +set(middleware_src_path "${middleware_unzip_path}/sophgo-middleware-${middleware_version_str}") + + +set(source_dir "src") +append_srcs_dir(ADD_SRCS "${source_dir}") + +# middleware +set(middleware_include_dir . + ${middleware_src_path}/v2/component/panel/${CONFIG_SOPHGO_MIDDLEWARE_CHIP} + ${middleware_src_path}/v2/include + ${middleware_src_path}/v2/include/isp/${CONFIG_SOPHGO_MIDDLEWARE_CHIP} + ${middleware_src_path}/v2/sample/common + ${middleware_src_path}/v2/uapi + ${middleware_src_path}/v2/3rdparty/inih + ${middleware_src_path}/v2/modules/ive/include/ +) +list(APPEND ADD_INCLUDE ${middleware_include_dir}) +set_property(SOURCE ${middleware_include_dir} PROPERTY GENERATED 1) + +append_srcs_dir(middleware_src_dir ${middleware_src_path}/v2/sample/common + ${middleware_src_path}/v2/component/isp/sensor/sg200x/lontium_lt6911 + ) +list(APPEND ADD_SRCS ${middleware_src_dir} + "${source_dir}/kvm_mmf.cpp") +set_property(SOURCE ${middleware_src_dir} PROPERTY GENERATED 1) + +set(middleware_static_lib_file "") + list(APPEND ADD_STATIC_LIB ${middleware_static_lib_file}) +set_property(SOURCE ${middleware_static_lib_file} PROPERTY GENERATED 1) + +if(${middleware_version_str} VERSION_EQUAL "0.0.4") + set(mmf_lib_dir ${middleware_src_path}/v2/lib) +else() + set(mmf_lib_dir ${middleware_src_path}/v2/${CONFIG_SOPHGO_MIDDLEWARE_CHIP}/lib_${CONFIG_SOPHGO_MIDDLEWARE_C_LIBRARY}_riscv64) +endif() +if(CONFIG_SOPHGO_MIDDLEWARE_C_LIBRARY STREQUAL "musl") + if(${middleware_version_str} VERSION_EQUAL "0.0.4") + set(middleware_dynamic_lib_file ${mmf_lib_dir}/libcvi_ive.so + ${mmf_lib_dir}/libcvi_bin.so + ${mmf_lib_dir}/libaaccomm2.so + ${mmf_lib_dir}/libaacdec2.so + ${mmf_lib_dir}/libaacenc2.so + ${mmf_lib_dir}/libaacsbrdec2.so + ${mmf_lib_dir}/libaacsbrenc2.so + ${mmf_lib_dir}/libae.so + ${mmf_lib_dir}/libaf.so + ${mmf_lib_dir}/libawb.so + ${mmf_lib_dir}/libcvi_audio.so + ${mmf_lib_dir}/libcvi_bin_isp.so + ${mmf_lib_dir}/libcvi_bin.so + ${mmf_lib_dir}/libcvi_ispd2.so + ${mmf_lib_dir}/libcvi_RES1.so + ${mmf_lib_dir}/libcvi_ssp.so + ${mmf_lib_dir}/libcvi_VoiceEngine.so + ${mmf_lib_dir}/libcvi_vqe.so + ${mmf_lib_dir}/libdnvqe.so + ${mmf_lib_dir}/libisp_algo.so + ${mmf_lib_dir}/libisp.so + ${mmf_lib_dir}/libmipi_tx.so + ${mmf_lib_dir}/libmisc.so + ${mmf_lib_dir}/libosdc.so + ${mmf_lib_dir}/libraw_dump.so + ${mmf_lib_dir}/libsys.so + ${mmf_lib_dir}/libvdec.so + ${mmf_lib_dir}/libvenc.so + ${mmf_lib_dir}/libvpu.so + ${mmf_lib_dir}/libjson-c.so.5 + ${mmf_lib_dir}/libtinyalsa.so + ${mmf_lib_dir}/3rd/libcli.so + ${mmf_lib_dir}/3rd/libini.so) + else() + set(middleware_dynamic_lib_file ${mmf_lib_dir}/libcvi_ive.so + ${mmf_lib_dir}/libcvi_bin.so + ${mmf_lib_dir}/libaaccomm2.so + ${mmf_lib_dir}/libaacdec2.so + ${mmf_lib_dir}/libaacenc2.so + ${mmf_lib_dir}/libaacsbrdec2.so + ${mmf_lib_dir}/libaacsbrenc2.so + ${mmf_lib_dir}/libae.so + ${mmf_lib_dir}/libaf.so + ${mmf_lib_dir}/libawb.so + ${mmf_lib_dir}/libcipher.so + ${mmf_lib_dir}/libcvi_audio.so + ${mmf_lib_dir}/libcvi_bin_isp.so + ${mmf_lib_dir}/libcvi_bin.so + ${mmf_lib_dir}/libcvi_ispd2.so + ${mmf_lib_dir}/libcvi_RES1.so + ${mmf_lib_dir}/libcvi_ssp.so + ${mmf_lib_dir}/libcvi_VoiceEngine.so + ${mmf_lib_dir}/libcvi_vqe.so + ${mmf_lib_dir}/libdnvqe.so + ${mmf_lib_dir}/libisp_algo.so + ${mmf_lib_dir}/libisp.so + ${mmf_lib_dir}/libmipi_tx.so + ${mmf_lib_dir}/libmisc.so + ${mmf_lib_dir}/libosdc.so + ${mmf_lib_dir}/libraw_dump.so + # ${mmf_lib_dir}/libraw_replay.so + # ${mmf_lib_dir}/libsns_full.so + # ${mmf_lib_dir}/libsns_gc4653.so + ${mmf_lib_dir}/libsys.so + ${mmf_lib_dir}/libvdec.so + ${mmf_lib_dir}/libvenc.so + ${mmf_lib_dir}/libvpu.so + ${mmf_lib_dir}/libz.so + ${mmf_lib_dir}/libtinyalsa.so + ${mmf_lib_dir}/3rd/libcli.so + ${mmf_lib_dir}/3rd/libini.so + ${mmf_lib_dir}/3rd/libjson-c.so.5) + endif() +elseif(CONFIG_SOPHGO_MIDDLEWARE_C_LIBRARY STREQUAL "glibc") + set(middleware_dynamic_lib_file ${mmf_lib_dir}/libcvi_vb.so + ${mmf_lib_dir}/libcvi_sys.so + ${mmf_lib_dir}/libcvi_debug.so + ) +else() + message(FATAL_ERROR "No sophgo middleware c library support, please check menuoconfig to select c library") +endif() +list(APPEND ADD_DYNAMIC_LIB ${middleware_dynamic_lib_file}) +set_property(SOURCE ${middleware_dynamic_lib_file} PROPERTY GENERATED 1) + +list(APPEND ADD_DEFINITIONS_PRIVATE -DSENSOR_LONTIUM_LT6911 + -DSENSOR2_TYPE=LONTIUM_LT6911_2M_60FPS_8BIT) + + +register_component(DYNAMIC) + + + +# Config enable component2 or not in Kconfig +################# Add include ################# +# list(APPEND ADD_INCLUDE "include" +# ) +# list(APPEND ADD_PRIVATE_INCLUDE "include_private") +############################################### + +############## Add source files ############### +# list(APPEND ADD_SRCS "src/lib2.c" +# ) +# FILE(GLOB_RECURSE EXTRA_SRC "src/*.c") +# FILE(GLOB EXTRA_SRC "src/*.c") +# list(APPEND ADD_SRCS ${EXTRA_SRC}) +# aux_source_directory(src ADD_SRCS) # collect all source file in src dir, will set var ADD_SRCS +# append_srcs_dir(ADD_SRCS "src") # append source file in src dir to var ADD_SRCS +# list(REMOVE_ITEM COMPONENT_SRCS "src/test.c") +# set(ADD_ASM_SRCS "src/asm.S") +# list(APPEND ADD_SRCS ${ADD_ASM_SRCS}) +# SET_PROPERTY(SOURCE ${ADD_ASM_SRCS} PROPERTY LANGUAGE C) # set .S ASM file as C language +# SET_SOURCE_FILES_PROPERTIES(${ADD_ASM_SRCS} PROPERTIES COMPILE_FLAGS "-x assembler-with-cpp -D BBBBB") +############################################### + + +###### Add required/dependent components ###### +# list(APPEND ADD_REQUIREMENTS basic ini) +############################################### + +###### Add link search path for requirements/libs ###### +# list(APPEND ADD_LINK_SEARCH_PATH "${CONFIG_TOOLCHAIN_PATH}/lib") +# list(APPEND ADD_REQUIREMENTS pthread m) # add system libs, pthread and math lib for example here +# set (OpenCV_DIR opencv/lib/cmake/opencv4) +# find_package(OpenCV REQUIRED) +############################################### + +############ Add static libs ################## +# list(APPEND ADD_STATIC_LIB "lib/libtest.a") +############################################### + +############ Add dynamic libs ################## +# list(APPEND ADD_DYNAMIC_LIB "lib/arch/v831/libmaix_nn.so" +# "lib/arch/v831/libmaix_cam.so" +# ) +############################################### + +#### Add compile option for this component #### +#### Just for this component, won't affect other +#### modules, including component that depend +#### on this component +# list(APPEND ADD_DEFINITIONS_PRIVATE -DAAAAA=1) + +#### Add compile option for this component +#### and components denpend on this component +# list(APPEND ADD_DEFINITIONS -DAAAAA222=1 +# -DAAAAA333=1) +############################################### + +############ Add static libs ################## +#### Update parent's variables like CMAKE_C_LINK_FLAGS +# set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--start-group libmaix/libtest.a -ltest2 -Wl,--end-group" PARENT_SCOPE) +############################################### + +# register component, DYNAMIC or SHARED flags will make component compiled to dynamic(shared) lib +# register_component() + diff --git a/vision/components/kvm_mmf/Kconfig b/vision/components/kvm_mmf/Kconfig new file mode 100644 index 0000000..b4b5a07 --- /dev/null +++ b/vision/components/kvm_mmf/Kconfig @@ -0,0 +1,32 @@ +# sophgo middleware version major minor patch +menu "sophgo middleware version" +config SOPHGO_MIDDLEWARE_CHIP + string "sophgo chip" + default "cv181x" + help + sophgo chip. cv180x or cv181x + +config SOPHGO_MIDDLEWARE_C_LIBRARY + string "sophgo c standard library" + default "musl" + help + sophgo c standard library. glibc or musl + +config SOPHGO_MIDDLEWARE_VERSION_MAJOR + int "sophgo middleware package major version" + default 0 + help + sophgo middleware package major version, 0 means auto select according to board + +config SOPHGO_MIDDLEWARE_VERSION_MINOR + int "sophgo middleware package minor version" + default 0 + help + sophgo middleware package minor version + +config SOPHGO_MIDDLEWARE_VERSION_PATCH + int "sophgo middleware package patch version" + default 4 + help + sophgo middleware package patch version +endmenu diff --git a/vision/components/kvm_mmf/include/kvm_mmf.hpp b/vision/components/kvm_mmf/include/kvm_mmf.hpp new file mode 100644 index 0000000..b98dd12 --- /dev/null +++ b/vision/components/kvm_mmf/include/kvm_mmf.hpp @@ -0,0 +1,68 @@ +#ifndef __KVM_MMF_HPP__ +#define __KVM_MMF_HPP__ + +#include "stdint.h" + +typedef struct { + uint8_t *data[8]; + int data_size[8]; + int count; +} mmf_stream_t; + +typedef struct { + uint8_t type; // 0, jpg; 1, h265; 2, h264 + int w; + int h; + int fmt; + uint8_t jpg_quality; // jpeg + int gop; // h264 + int intput_fps; // h264 + int output_fps; // h264 + int bitrate; // h264 +} mmf_venc_cfg_t; + +// init sys +int mmf_init(void); +int mmf_deinit(void); +int mmf_try_deinit(bool force); +bool mmf_is_init(void); + +// manage vi channels(vi->vpssgroup->vpss->frame) +int mmf_get_vi_unused_channel(void); +int mmf_vi_init(void); +int mmf_vi_deinit(void); +int mmf_add_vi_channel_with_enc(int ch, int width, int height, int format); +int mmf_add_vi_channel(int ch, int width, int height, int format); +int mmf_del_vi_channel(int ch); +int mmf_del_vi_channel_all(void); +int mmf_reset_vi_channel(int ch, int width, int height, int format); +bool mmf_vi_chn_is_open(int ch); +int mmf_vi_aligned_width(int ch); +void mmf_set_vi_hmirror(int ch, bool en); +void mmf_set_vi_vflip(int ch, bool en); +void mmf_get_vi_hmirror(int ch, bool *en); +void mmf_get_vi_vflip(int ch, bool *en); + +// get vi frame +int mmf_vi_frame_pop(int ch, void **data, int *len, int *width, int *height, int *format); +void mmf_vi_frame_free(int ch); + +// invert format +int mmf_invert_format_to_maix(int mmf_format); +int mmf_invert_format_to_mmf(int maix_format); + +// venc +int mmf_enc_jpg_init(int ch, int w, int h, int format, int quality); +int mmf_enc_jpg_deinit(int ch); +int mmf_enc_jpg_push(int ch, uint8_t *data, int w, int h, int format); +int mmf_enc_jpg_push_with_quality(int ch, uint8_t *data, int w, int h, int format, int quality); +int mmf_enc_jpg_pop(int ch, uint8_t **data, int *size); +int mmf_enc_jpg_free(int ch); +int mmf_add_venc_channel(int ch, mmf_venc_cfg_t *cfg); +int mmf_del_venc_channel(int ch); +int mmf_del_venc_channel_all(); +int mmf_venc_push(int ch, uint8_t *data, int w, int h, int format); +int mmf_venc_pop(int ch, mmf_stream_t *stream); +int mmf_venc_free(int ch); + +#endif // __KVM_MMF_HPP__ diff --git a/vision/components/kvm_mmf/src/kvm_mmf.cpp b/vision/components/kvm_mmf/src/kvm_mmf.cpp new file mode 100644 index 0000000..054d812 --- /dev/null +++ b/vision/components/kvm_mmf/src/kvm_mmf.cpp @@ -0,0 +1,2352 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "math.h" +#include + +#include /* low-level i/o */ +#include "cvi_buffer.h" +#include "cvi_ae_comm.h" +#include "cvi_awb_comm.h" +#include "cvi_comm_isp.h" +#include "cvi_comm_sns.h" +#include "cvi_ae.h" +#include "cvi_awb.h" +#include "cvi_isp.h" +#include "cvi_sns_ctrl.h" +#include "cvi_ive.h" +#include "cvi_sys.h" +#include "sample_comm.h" +#include "kvm_mmf.hpp" +#include "global_config.h" + +#define MMF_VI_MAX_CHN 2 // manually limit the max channel number of vi +#define MMF_RGN_MAX_NUM 16 +#define MMF_VENC_MAX_CHN 4 + +#define MMF_VB_VI_ID 0 + +#if VPSS_MAX_PHY_CHN_NUM < MMF_VI_MAX_CHN +#error "VPSS_MAX_PHY_CHN_NUM < MMF_VI_MAX_CHN" +#endif + +typedef struct { + uint8_t ch; + SIZE_S input; + SIZE_S output; + int fps; + uint8_t depth; + uint8_t fit; // fit = 0, width to new width, height to new height, may be stretch + // fit = 1, keep aspect ratio, fill blank area with black color + // fit = 2, keep aspect ratio, crop image to fit new size + int input_fmt; + int output_fmt; +} vpss_info_t; + +typedef struct { + uint8_t ch; + uint8_t type; // 0, jpg; 1, h265; 2, h264 + uint8_t is_inited; + uint8_t is_used; + uint8_t is_running; + uint8_t use_vpss; + VIDEO_FRAME_INFO_S *capture_frame; + VENC_STREAM_S capture_stream; + mmf_venc_cfg_t cfg; + vpss_info_t vpss; + uint32_t pool_id; +} venc_info_t; + +typedef enum { + MMF_MOD_VO, + MMF_MOD_VI, + MMF_MOD_VPSS, + MMF_MOD_VENC, + MMF_MOD_VDEC, + MMF_MOD_REGION, +} mmf_mod_type_t; + +typedef struct { + char name[15]; + uint8_t is_used; + mmf_mod_type_t mod; + uint32_t pool_id; + uint32_t size; + uint32_t max_num; +} mmf_vb_pool_t; + +typedef struct { + int mmf_used_cnt; + bool vi_is_inited; + bool vi_chn_is_inited[MMF_VI_MAX_CHN]; + int vi_chn_pool_id[MMF_VI_MAX_CHN]; + SIZE_S vi_size; + VIDEO_FRAME_INFO_S vi_frame[MMF_VI_MAX_CHN]; + VB_CONFIG_S vb_conf; + + int ive_is_init; + IVE_HANDLE ive_handle; + IVE_IMAGE_S ive_rgb2yuv_rgb_img; + IVE_IMAGE_S ive_rgb2yuv_yuv_img; + int ive_rgb2yuv_w; + int ive_rgb2yuv_h; + + bool rgn_is_init[MMF_RGN_MAX_NUM]; + bool rgn_is_bind[MMF_RGN_MAX_NUM]; + RGN_TYPE_E rgn_type[MMF_RGN_MAX_NUM]; + int rgn_id[MMF_RGN_MAX_NUM]; + MOD_ID_E rgn_mod_id[MMF_RGN_MAX_NUM]; + CVI_S32 rgn_dev_id[MMF_RGN_MAX_NUM]; + CVI_S32 rgn_chn_id[MMF_RGN_MAX_NUM]; + uint8_t* rgn_canvas_data[MMF_RGN_MAX_NUM]; + int rgn_canvas_w[MMF_RGN_MAX_NUM]; + int rgn_canvas_h[MMF_RGN_MAX_NUM]; + int rgn_canvas_format[MMF_RGN_MAX_NUM]; + + int enc_jpg_is_init; + VENC_STREAM_S enc_jpeg_frame; + int enc_jpg_frame_w; + int enc_jpg_frame_h; + int enc_jpg_frame_fmt; + int enc_jpg_running; + int enc_jpg_quality; + VIDEO_FRAME_INFO_S *enc_jpg_frame; + int enc_jpg_input_pool_id; + int enc_jpg_output_pool_id; + + int vb_of_vi_is_config : 1; + int vb_of_private_is_config : 1; + int vb_size_of_vi; + int vb_count_of_vi; + int vb_size_of_private; + int vb_count_of_private; + + + SAMPLE_SNS_TYPE_E sensor_type; + + venc_info_t venc[MMF_VENC_MAX_CHN]; + uint8_t h265_or_h264_is_used; + + mmf_vb_pool_t vb_pool[VB_MAX_COMM_POOLS]; +} priv_t; + +typedef struct { + int enc_jpg_enable : 1; + bool vi_hmirror[MMF_VI_MAX_CHN]; + bool vi_vflip[MMF_VI_MAX_CHN]; +} g_priv_t; + +static priv_t priv; +static g_priv_t g_priv; + +#define MODULE_NAME "soph_vi" + +static int _is_module_in_use(const char *module_name) { + FILE *fp; + char buffer[256]; + + fp = fopen("/proc/modules", "r"); + if (fp == NULL) { + perror("fopen"); + return -1; + } + + while (fgets(buffer, sizeof(buffer), fp) != NULL) { + char mod_name[256]; + int usage_count; + + sscanf(buffer, "%255s %*s %d", mod_name, &usage_count); + + if (strcmp(mod_name, module_name) == 0) { + fclose(fp); + return usage_count > 0; + } + } + + fclose(fp); + return 0; +} + +static int reinit_soph_vb(void) +{ + printf("mmf insmod..\r\n"); + system("rmmod soph_ive soph_vc_driver soph_rgn soph_dwa soph_vpss soph_vi soph_snsr_i2c soph_mipi_rx soph_fast_image soph_rtos_cmdqu soph_base"); + // system("insmod /mnt/system/ko/soph_sys.ko"); + system("insmod /mnt/system/ko/soph_base.ko"); + system("insmod /mnt/system/ko/soph_rtos_cmdqu.ko"); + system("insmod /mnt/system/ko/soph_fast_image.ko"); + system("insmod /mnt/system/ko/soph_mipi_rx.ko"); + system("insmod /mnt/system/ko/soph_snsr_i2c.ko"); + system("insmod /mnt/system/ko/soph_vi.ko"); + system("insmod /mnt/system/ko/soph_vpss.ko"); + system("insmod /mnt/system/ko/soph_dwa.ko"); + system("insmod /mnt/system/ko/soph_rgn.ko"); + system("insmod /mnt/system/ko/soph_vc_driver.ko"); + system("insmod /mnt/system/ko/soph_ive.ko"); + + return 0; +} + +static int _get_vb_pool_cnt(void) +{ + FILE *file; + char line[1024]; + int poolIdCount = 0; + + file = fopen("/proc/cvitek/vb", "r"); + if (file == NULL) { + perror("can not open /proc/cvitek/vb"); + return 0; + } + + while (fgets(line, sizeof(line), file)) { + if (strstr(line, "PoolId(") != NULL) { + poolIdCount++; + } + } + + fclose(file); + return poolIdCount; +} + +static int _create_vb_pool(char *name, mmf_mod_type_t mod, uint32_t size, uint32_t max_num) +{ + uint32_t pool_id = -1; + VB_POOL_CONFIG_S stVbPoolCfg; + stVbPoolCfg.u32BlkCnt = max_num; + stVbPoolCfg.u32BlkSize = size; + stVbPoolCfg.enRemapMode = VB_REMAP_MODE_CACHED; + + if (max_num == 0 || size == 0) { + return -1; + } + + pool_id = CVI_VB_CreatePool(&stVbPoolCfg); + if (pool_id == VB_INVALID_POOLID || pool_id >= VB_MAX_COMM_POOLS) { + return -2; + } + + mmf_vb_pool_t *info = (mmf_vb_pool_t *)&priv.vb_pool[pool_id]; + info->pool_id = pool_id; + strncpy(info->name, name, sizeof(info->name)); + info->mod = mod; + info->size = size; + info->max_num = max_num; + info->is_used = 1; + + return pool_id; +} + +static int _destroy_vb_pool(uint32_t pool_id) +{ + CVI_S32 s32Ret; + mmf_vb_pool_t *info = (mmf_vb_pool_t *)&priv.vb_pool[pool_id]; + if (info->is_used) { + s32Ret = CVI_VB_DestroyPool(pool_id); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VB_DestroyPool : %d fail!\n", pool_id); + return -1; + } + memset(info, 0, sizeof(mmf_vb_pool_t)); + } + + return 0; +} + +__attribute__((unused)) static void _list_vb_pool(void) +{ + printf("====== VB POOL =======\r\n"); + for (int pool_id = 0; pool_id < VB_MAX_COMM_POOLS; pool_id ++) { + mmf_vb_pool_t *info = (mmf_vb_pool_t *)&priv.vb_pool[pool_id]; + if (info->is_used) { + printf("[%d] name:%s size:%d num:%d\r\n", pool_id, info->name, info->size, info->max_num); + } + } + printf("\r\n"); +} + +static SAMPLE_VI_CONFIG_S g_stViConfig; +static SAMPLE_INI_CFG_S g_stIniCfg; +static CVI_S32 _mmf_vpss_deinit_new(VPSS_GRP VpssGrp); + +static int _free_leak_memory_of_ion(void) +{ + #define MAX_LINE_LENGTH 256 + FILE *fp; + char line[MAX_LINE_LENGTH]; + char alloc_buf_size_str[20], phy_addr_str[20], buffer_name[20]; + int alloc_buf_size; + uint64_t phy_addr; + + fp = fopen("/sys/kernel/debug/ion/cvi_carveout_heap_dump/summary", "r"); + if (fp == NULL) { + fprintf(stderr, "Error opening file\n"); + return 1; + } + + while (fgets(line, MAX_LINE_LENGTH, fp) != NULL) { + if (sscanf(line, "%*d %s %s %*d %s", alloc_buf_size_str, phy_addr_str, buffer_name) == 3) { + printf("[ION] %s %s %s\r\n", alloc_buf_size_str, phy_addr_str, buffer_name); + // FIXME: release jpeg_ion + if (strcmp(buffer_name, "VI_DMA_BUF") + && strcmp(buffer_name, "ISP_SHARED_BUFFER_0")) + continue; + struct sys_ion_data_new ion_data = { + .cached = 1, + .dmabuf_fd = (uint32_t)-1, + }; + + alloc_buf_size = atoi(alloc_buf_size_str); + phy_addr = (unsigned int)strtol(phy_addr_str, NULL, 16); + + ion_data.size = alloc_buf_size; + ion_data.addr_p = phy_addr; + memset(ion_data.name, 0, sizeof(ion_data.name)); + strcpy((char *)ion_data.name, buffer_name); + + printf("alloc_buf_size(%s): %d, phy_addr(%s): %#lx, buffer_name: %s\n", + alloc_buf_size_str, alloc_buf_size, phy_addr_str, phy_addr, buffer_name); + + printf("ion_data.size:%d, ion_data.addr_p:%#x, ion_data.name:%s\r\n", ion_data.size, (int)ion_data.addr_p, ion_data.name); + + int res = ionFree(&ion_data); + if (res) { + printf("ionFree failed! res:%#x\r\n", res); + mmf_deinit(); + return -1; + } + + } + } + + fclose(fp); + + return 0; +} + +static int _free_leak_memory_of_vb(void) { + #define MAX_LINE_LENGTH 256 + FILE *fp; + char line[MAX_LINE_LENGTH]; + int pool_id = 0; + uint64_t phy_addr = 0; + int blk_cnt = 0; + int blk_size = 0; + int free_cnt = 0; + + fp = fopen("/proc/cvitek/vb", "r"); + if (fp == NULL) { + fprintf(stderr, "Error opening file\n"); + return 1; + } + + while (fgets(line, MAX_LINE_LENGTH, fp) != NULL) { + + if (strstr(line, "PoolId :")) { + sscanf(line, "%*s : %d", &pool_id); + } else if (strstr(line, "PhysAddr :")) { + sscanf(line, "%*s : %lx", &phy_addr); + } else if (strstr(line, "BlkSz :")) { + sscanf(line, "%*s : %d", &blk_size); + } else if (strstr(line, "BlkCnt : ")) { + sscanf(line, "%*s : %d", &blk_cnt); + } else if (strstr(line, "Free :")) { + sscanf(line, "%*s : %d", &free_cnt); + + CVI_SYS_Exit(); + CVI_VB_Exit(); + + if (free_cnt != blk_cnt) { + printf("relese PoolId: %d, PhysAddr: 0x%lx, BlkSize: %d, BlkCnt: %d Free: %d\n", + pool_id, phy_addr, blk_size, blk_cnt, free_cnt); + for (int i = 0; i < blk_cnt; i ++) { + uint64_t try_release_phy_addr = phy_addr + i * blk_size; + printf("try release poolid:%d phy:%#lx\r\n", pool_id, try_release_phy_addr); + VB_BLK blk = CVI_VB_PhysAddr2Handle(try_release_phy_addr); + if (0 != CVI_VB_ReleaseBlock(blk)) { + printf("release poolid:%d phy:%#lx failed!\r\n", pool_id, try_release_phy_addr); + } + } + } + } + } + + fclose(fp); + + return 0; +} + +static inline CVI_VOID VENC_GetPicBufferConfig2(CVI_U32 u32Width, CVI_U32 u32Height, + PIXEL_FORMAT_E enPixelFormat, DATA_BITWIDTH_E enBitWidth, COMPRESS_MODE_E enCmpMode, + VB_CAL_CONFIG_S *pstVbCfg) +{ + CVI_U32 u32AlignWidth = ALIGN(u32Width, VENC_ALIGN_W); + CVI_U32 u32AlignHeight = u32Height; + CVI_U32 u32Align = VENC_ALIGN_W; + + COMMON_GetPicBufferConfig(u32AlignWidth, u32AlignHeight, enPixelFormat, + enBitWidth, enCmpMode, u32Align, pstVbCfg); +} + +static VIDEO_FRAME_INFO_S *_mmf_alloc_frame(int id, SIZE_S stSize, PIXEL_FORMAT_E enPixelFormat) +{ + VIDEO_FRAME_INFO_S *pstVideoFrame; + VIDEO_FRAME_S *pstVFrame; + VB_BLK blk; + VB_CAL_CONFIG_S stVbCfg; + + pstVideoFrame = (VIDEO_FRAME_INFO_S *)calloc(sizeof(*pstVideoFrame), 1); + if (pstVideoFrame == NULL) { + SAMPLE_PRT("Failed to allocate VIDEO_FRAME_INFO_S\n"); + return NULL; + } + + memset(&stVbCfg, 0, sizeof(stVbCfg)); + VENC_GetPicBufferConfig2(stSize.u32Width, + stSize.u32Height, + enPixelFormat, + DATA_BITWIDTH_8, + COMPRESS_MODE_NONE, + &stVbCfg); + + pstVFrame = &pstVideoFrame->stVFrame; + + pstVFrame->enCompressMode = COMPRESS_MODE_NONE; + pstVFrame->enPixelFormat = enPixelFormat; + pstVFrame->enVideoFormat = VIDEO_FORMAT_LINEAR; + pstVFrame->enColorGamut = COLOR_GAMUT_BT709; + pstVFrame->u32Width = stSize.u32Width; + pstVFrame->u32Height = stSize.u32Height; + pstVFrame->u32TimeRef = 0; + pstVFrame->u64PTS = 0; + pstVFrame->enDynamicRange = DYNAMIC_RANGE_SDR8; + + blk = CVI_VB_GetBlock(id, stVbCfg.u32VBSize); + if (blk == VB_INVALID_HANDLE) { + SAMPLE_PRT("Can't acquire vb block. id: %d size:%d\n", id, stVbCfg.u32VBSize); + free(pstVideoFrame); + return NULL; + } + + pstVideoFrame->u32PoolId = CVI_VB_Handle2PoolId(blk); + pstVFrame->u64PhyAddr[0] = CVI_VB_Handle2PhysAddr(blk); + pstVFrame->u32Stride[0] = stVbCfg.u32MainStride; + pstVFrame->u32Length[0] = stVbCfg.u32MainYSize; + pstVFrame->pu8VirAddr[0] = (CVI_U8 *)CVI_SYS_MmapCache(pstVFrame->u64PhyAddr[0], stVbCfg.u32VBSize); + + if (stVbCfg.plane_num > 1) { + pstVFrame->u64PhyAddr[1] = ALIGN(pstVFrame->u64PhyAddr[0] + stVbCfg.u32MainYSize, stVbCfg.u16AddrAlign); + pstVFrame->u32Stride[1] = stVbCfg.u32CStride; + pstVFrame->u32Length[1] = stVbCfg.u32MainCSize; + pstVFrame->pu8VirAddr[1] = (CVI_U8 *)pstVFrame->pu8VirAddr[0] + pstVFrame->u32Length[0]; + } + + if (stVbCfg.plane_num > 2) { + pstVFrame->u64PhyAddr[2] = ALIGN(pstVFrame->u64PhyAddr[1] + stVbCfg.u32MainCSize, stVbCfg.u16AddrAlign); + pstVFrame->u32Stride[2] = stVbCfg.u32CStride; + pstVFrame->u32Length[2] = stVbCfg.u32MainCSize; + pstVFrame->pu8VirAddr[2] = (CVI_U8 *)pstVFrame->pu8VirAddr[1] + pstVFrame->u32Length[1]; + } + + // CVI_VENC_TRACE("phy addr(%#llx, %#llx, %#llx), Size %x\n", (long long)pstVFrame->u64PhyAddr[0] + // , (long long)pstVFrame->u64PhyAddr[1], (long long)pstVFrame->u64PhyAddr[2], stVbCfg.u32VBSize); + // CVI_VENC_TRACE("vir addr(%p, %p, %p), Size %x\n", pstVFrame->pu8VirAddr[0] + // , pstVFrame->pu8VirAddr[1], pstVFrame->pu8VirAddr[2], stVbCfg.u32MainSize); + + return pstVideoFrame; +} + +static CVI_S32 _mmf_free_frame(VIDEO_FRAME_INFO_S *pstVideoFrame) +{ + VIDEO_FRAME_S *pstVFrame = &pstVideoFrame->stVFrame; + VB_BLK blk; + + if (pstVFrame->pu8VirAddr[0]) + CVI_SYS_Munmap((CVI_VOID *)pstVFrame->pu8VirAddr[0], pstVFrame->u32Length[0]); + if (pstVFrame->pu8VirAddr[1]) + CVI_SYS_Munmap((CVI_VOID *)pstVFrame->pu8VirAddr[1], pstVFrame->u32Length[1]); + if (pstVFrame->pu8VirAddr[2]) + CVI_SYS_Munmap((CVI_VOID *)pstVFrame->pu8VirAddr[2], pstVFrame->u32Length[2]); + + blk = CVI_VB_PhysAddr2Handle(pstVFrame->u64PhyAddr[0]); + if (blk != VB_INVALID_HANDLE) { + CVI_VB_ReleaseBlock(blk); + } + + free(pstVideoFrame); + + return CVI_SUCCESS; +} + +static int cvi_ive_init(void) +{ + CVI_S32 s32Ret; + if (priv.ive_is_init) + return 0; + + priv.ive_rgb2yuv_w = 640; + priv.ive_rgb2yuv_h = 480; + priv.ive_handle = CVI_IVE_CreateHandle(); + if (priv.ive_handle == NULL) { + printf("CVI_IVE_CreateHandle failed!\n"); + return -1; + } + + s32Ret = CVI_IVE_CreateImage_Cached(priv.ive_handle, &priv.ive_rgb2yuv_rgb_img, IVE_IMAGE_TYPE_U8C3_PACKAGE, priv.ive_rgb2yuv_w, priv.ive_rgb2yuv_h); + if (s32Ret != CVI_SUCCESS) { + printf("Create src image failed!\n"); + CVI_IVE_DestroyHandle(priv.ive_handle); + return -1; + } + + s32Ret = CVI_IVE_CreateImage_Cached(priv.ive_handle, &priv.ive_rgb2yuv_yuv_img, IVE_IMAGE_TYPE_YUV420SP, priv.ive_rgb2yuv_w, priv.ive_rgb2yuv_h); + if (s32Ret != CVI_SUCCESS) { + printf("Create src image failed!\n"); + CVI_IVE_DestroyHandle(priv.ive_handle); + return -1; + } + + priv.ive_is_init = 1; + return 0; +} + +static int cvi_ive_deinit(void) +{ + if (priv.ive_is_init == 0) + return 0; + + CVI_SYS_FreeI(priv.ive_handle, &priv.ive_rgb2yuv_rgb_img); + CVI_SYS_FreeI(priv.ive_handle, &priv.ive_rgb2yuv_yuv_img); + CVI_IVE_DestroyHandle(priv.ive_handle); + + priv.ive_is_init = 0; + return 0; +} + +static int cvi_rgb2nv21(uint8_t *src, int input_w, int input_h) +{ + CVI_S32 s32Ret; + + int width = ALIGN(input_w, DEFAULT_ALIGN); + int height = input_h; + + if (width != priv.ive_rgb2yuv_w || height != priv.ive_rgb2yuv_h) { + CVI_SYS_FreeI(priv.ive_handle, &priv.ive_rgb2yuv_rgb_img); + CVI_SYS_FreeI(priv.ive_handle, &priv.ive_rgb2yuv_yuv_img); + priv.ive_rgb2yuv_w = width; + priv.ive_rgb2yuv_h = height; + printf("reinit rgb2nv21 buffer, buff w:%d h:%d\n", priv.ive_rgb2yuv_w, priv.ive_rgb2yuv_h); + s32Ret = CVI_IVE_CreateImage_Cached(priv.ive_handle, &priv.ive_rgb2yuv_rgb_img, IVE_IMAGE_TYPE_U8C3_PACKAGE, priv.ive_rgb2yuv_w, priv.ive_rgb2yuv_h); + if (s32Ret != CVI_SUCCESS) { + printf("Create src image failed!\n"); + return -1; + } + + s32Ret = CVI_IVE_CreateImage_Cached(priv.ive_handle, &priv.ive_rgb2yuv_yuv_img, IVE_IMAGE_TYPE_YUV420SP, priv.ive_rgb2yuv_w, priv.ive_rgb2yuv_h); + if (s32Ret != CVI_SUCCESS) { + printf("Create src image failed!\n"); + return -1; + } + } + + if (width != input_w) { + for (int h = 0; h < height; h++) { + memcpy((uint8_t *)priv.ive_rgb2yuv_rgb_img.u64VirAddr[0] + width * h * 3, (uint8_t *)src + input_w * h * 3, input_w * 3); + } + } else { + memcpy((uint8_t *)priv.ive_rgb2yuv_rgb_img.u64VirAddr[0], (uint8_t *)src, width * height * 3); + } + + IVE_CSC_CTRL_S stCtrl; + stCtrl.enMode = IVE_CSC_MODE_VIDEO_BT601_RGB2YUV; + s32Ret = CVI_IVE_CSC(priv.ive_handle, &priv.ive_rgb2yuv_rgb_img, &priv.ive_rgb2yuv_yuv_img, &stCtrl, 1); + if (s32Ret != CVI_SUCCESS) { + printf("Run HW IVE CSC YUV2RGB failed!\n"); + return -1; + } + return 0; +} + +static int _try_release_sys(void) +{ + CVI_S32 s32Ret = CVI_FAILURE; + SAMPLE_INI_CFG_S stIniCfg; + SAMPLE_VI_CONFIG_S stViConfig; + if (SAMPLE_COMM_VI_ParseIni(&stIniCfg)) { + SAMPLE_PRT("Parse complete\n"); + return s32Ret; + } + + priv.sensor_type = stIniCfg.enSnsType[0]; + + s32Ret = CVI_VI_SetDevNum(stIniCfg.devNum); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("VI_SetDevNum failed with %#x\n", s32Ret); + return s32Ret; + } + + s32Ret = SAMPLE_COMM_VI_IniToViCfg(&stIniCfg, &stViConfig); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("SAMPLE_COMM_VI_IniToViCfg failed with %#x\n", s32Ret); + return s32Ret; + } + + s32Ret = SAMPLE_COMM_VI_DestroyIsp(&stViConfig); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("SAMPLE_COMM_VI_DestroyIsp failed with %#x\n", s32Ret); + return s32Ret; + } + + s32Ret = SAMPLE_COMM_VI_DestroyVi(&stViConfig); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("SAMPLE_COMM_VI_DestroyVi failed with %#x\n", s32Ret); + return s32Ret; + } + + SAMPLE_COMM_SYS_Exit(); + return s32Ret; +} + +int _try_release_vi(void) +{ + CVI_S32 s32Ret = CVI_FAILURE; + s32Ret = mmf_del_vi_channel_all(); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("mmf_del_vi_channel_all failed with %#x\n", s32Ret); + return s32Ret; + } + return s32Ret; +} + +int _try_release_venc_all(void) +{ + for (int ch = 0; ch < VENC_MAX_CHN_NUM; ch ++) { + CVI_VENC_StopRecvFrame(ch); + CVI_VENC_ResetChn(ch); + CVI_VENC_DestroyChn(ch); + } + return 0; +} + +int _try_release_vpss_all(void) +{ + for (int ch = 0; ch < 4; ch ++) { + _mmf_vpss_deinit_new(ch); + } + return 0; +} + +static void _mmf_sys_exit(void) +{ + if (g_stViConfig.s32WorkingViNum != 0) { + SAMPLE_COMM_VI_DestroyIsp(&g_stViConfig); + SAMPLE_COMM_VI_DestroyVi(&g_stViConfig); + } + SAMPLE_COMM_SYS_Exit(); +} + +static CVI_S32 _mmf_sys_init(SIZE_S stSize) +{ + VB_CONFIG_S stVbConf; + CVI_U32 u32BlkSize, u32BlkRotSize; + CVI_S32 s32Ret = CVI_SUCCESS; + COMPRESS_MODE_E enCompressMode = COMPRESS_MODE_NONE; + + memset(&stVbConf, 0, sizeof(VB_CONFIG_S)); + memcpy(&stVbConf, &priv.vb_conf, sizeof(VB_CONFIG_S)); + + // vi + u32BlkSize = COMMON_GetPicBufferSize(stSize.u32Width, stSize.u32Height, PIXEL_FORMAT_UYVY, + DATA_BITWIDTH_12, enCompressMode, DEFAULT_ALIGN); + u32BlkRotSize = COMMON_GetPicBufferSize(stSize.u32Height, stSize.u32Width, PIXEL_FORMAT_UYVY, + DATA_BITWIDTH_12, enCompressMode, DEFAULT_ALIGN); + u32BlkSize = MAX(u32BlkSize, u32BlkRotSize); + stVbConf.astCommPool[MMF_VB_VI_ID].u32BlkSize = u32BlkSize; + stVbConf.astCommPool[MMF_VB_VI_ID].u32BlkCnt = 4; + stVbConf.astCommPool[MMF_VB_VI_ID].enRemapMode = VB_REMAP_MODE_CACHED; + stVbConf.u32MaxPoolCnt = 1; + + s32Ret = SAMPLE_COMM_SYS_Init(&stVbConf); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("system init failed with %#x\n", s32Ret); + goto error; + } + + return s32Ret; +error: + _mmf_sys_exit(); + return s32Ret; +} + +static CVI_S32 _mmf_vpss_deinit(VPSS_GRP VpssGrp, VPSS_CHN VpssChn) +{ + CVI_BOOL abChnEnable[VPSS_MAX_PHY_CHN_NUM] = {0}; + CVI_S32 s32Ret = CVI_SUCCESS; + + /*start vpss*/ + abChnEnable[VpssChn] = CVI_TRUE; + s32Ret = SAMPLE_COMM_VPSS_Stop(VpssGrp, abChnEnable); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("init vpss group failed. s32Ret: 0x%x !\n", s32Ret); + } + + return s32Ret; +} + +static CVI_S32 _mmf_vpss_deinit_new(VPSS_GRP VpssGrp) +{ + CVI_S32 s32Ret = CVI_SUCCESS; + + s32Ret = CVI_VPSS_StopGrp(VpssGrp); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("Vpss Stop Grp %d failed! Please check param\n", VpssGrp); + return CVI_FAILURE; + } + + s32Ret = CVI_VPSS_DestroyGrp(VpssGrp); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("Vpss Destroy Grp %d failed! Please check\n", VpssGrp); + return CVI_FAILURE; + } + + return s32Ret; +} + +// fit = 0, width to new width, height to new height, may be stretch +// fit = 1, keep aspect ratio, fill blank area with black color +// fit = other, keep aspect ratio, crop image to fit new size +static CVI_S32 _mmf_vpss_init(VPSS_GRP VpssGrp, VPSS_CHN VpssChn, SIZE_S stSizeIn, SIZE_S stSizeOut, PIXEL_FORMAT_E formatIn, PIXEL_FORMAT_E formatOut, +int fps, int depth, bool mirror, bool flip, int fit) +{ + VPSS_GRP_ATTR_S stVpssGrpAttr; + VPSS_CROP_INFO_S stGrpCropInfo; + CVI_BOOL abChnEnable[VPSS_MAX_PHY_CHN_NUM] = {0}; + VPSS_CHN_ATTR_S astVpssChnAttr[VPSS_MAX_PHY_CHN_NUM]; + CVI_S32 s32Ret = CVI_SUCCESS; + + memset(&stVpssGrpAttr, 0, sizeof(VPSS_GRP_ATTR_S)); + stVpssGrpAttr.stFrameRate.s32SrcFrameRate = -1; + stVpssGrpAttr.stFrameRate.s32DstFrameRate = -1; + stVpssGrpAttr.enPixelFormat = formatIn; + stVpssGrpAttr.u32MaxW = stSizeIn.u32Width; + stVpssGrpAttr.u32MaxH = stSizeIn.u32Height; + stVpssGrpAttr.u8VpssDev = 0; + + CVI_FLOAT corp_scale_w = (CVI_FLOAT)stSizeIn.u32Width / stSizeOut.u32Width; + CVI_FLOAT corp_scale_h = (CVI_FLOAT)stSizeIn.u32Height / stSizeOut.u32Height; + CVI_U32 crop_w = -1, crop_h = -1; + if (fit == 0) { + memset(astVpssChnAttr, 0, sizeof(VPSS_CHN_ATTR_S) * VPSS_MAX_PHY_CHN_NUM); + astVpssChnAttr[VpssChn].u32Width = stSizeOut.u32Width; + astVpssChnAttr[VpssChn].u32Height = stSizeOut.u32Height; + astVpssChnAttr[VpssChn].enVideoFormat = VIDEO_FORMAT_LINEAR; + astVpssChnAttr[VpssChn].enPixelFormat = formatOut; + astVpssChnAttr[VpssChn].stFrameRate.s32SrcFrameRate = fps; + astVpssChnAttr[VpssChn].stFrameRate.s32DstFrameRate = fps; + astVpssChnAttr[VpssChn].u32Depth = depth; + astVpssChnAttr[VpssChn].bMirror = mirror; + astVpssChnAttr[VpssChn].bFlip = flip; + astVpssChnAttr[VpssChn].stAspectRatio.enMode = ASPECT_RATIO_MANUAL; + astVpssChnAttr[VpssChn].stAspectRatio.stVideoRect.s32X = 0; + astVpssChnAttr[VpssChn].stAspectRatio.stVideoRect.s32Y = 0; + astVpssChnAttr[VpssChn].stAspectRatio.stVideoRect.u32Width = stSizeOut.u32Width; + astVpssChnAttr[VpssChn].stAspectRatio.stVideoRect.u32Height = stSizeOut.u32Height; + astVpssChnAttr[VpssChn].stAspectRatio.bEnableBgColor = CVI_TRUE; + astVpssChnAttr[VpssChn].stAspectRatio.u32BgColor = COLOR_RGB_BLACK; + astVpssChnAttr[VpssChn].stNormalize.bEnable = CVI_FALSE; + + stGrpCropInfo.bEnable = false; + } else if (fit == 1) { + memset(astVpssChnAttr, 0, sizeof(VPSS_CHN_ATTR_S) * VPSS_MAX_PHY_CHN_NUM); + astVpssChnAttr[VpssChn].u32Width = stSizeOut.u32Width; + astVpssChnAttr[VpssChn].u32Height = stSizeOut.u32Height; + astVpssChnAttr[VpssChn].enVideoFormat = VIDEO_FORMAT_LINEAR; + astVpssChnAttr[VpssChn].enPixelFormat = formatOut; + astVpssChnAttr[VpssChn].stFrameRate.s32SrcFrameRate = fps; + astVpssChnAttr[VpssChn].stFrameRate.s32DstFrameRate = fps; + astVpssChnAttr[VpssChn].u32Depth = depth; + astVpssChnAttr[VpssChn].bMirror = mirror; + astVpssChnAttr[VpssChn].bFlip = flip; + astVpssChnAttr[VpssChn].stAspectRatio.enMode = ASPECT_RATIO_AUTO; + astVpssChnAttr[VpssChn].stAspectRatio.bEnableBgColor = CVI_TRUE; + astVpssChnAttr[VpssChn].stAspectRatio.u32BgColor = COLOR_RGB_BLACK; + astVpssChnAttr[VpssChn].stNormalize.bEnable = CVI_FALSE; + + stGrpCropInfo.bEnable = false; + } else { + memset(astVpssChnAttr, 0, sizeof(VPSS_CHN_ATTR_S) * VPSS_MAX_PHY_CHN_NUM); + astVpssChnAttr[VpssChn].u32Width = stSizeOut.u32Width; + astVpssChnAttr[VpssChn].u32Height = stSizeOut.u32Height; + astVpssChnAttr[VpssChn].enVideoFormat = VIDEO_FORMAT_LINEAR; + astVpssChnAttr[VpssChn].enPixelFormat = formatOut; + astVpssChnAttr[VpssChn].stFrameRate.s32SrcFrameRate = fps; + astVpssChnAttr[VpssChn].stFrameRate.s32DstFrameRate = fps; + astVpssChnAttr[VpssChn].u32Depth = depth; + astVpssChnAttr[VpssChn].bMirror = mirror; + astVpssChnAttr[VpssChn].bFlip = flip; + astVpssChnAttr[VpssChn].stAspectRatio.enMode = ASPECT_RATIO_AUTO; + astVpssChnAttr[VpssChn].stAspectRatio.bEnableBgColor = CVI_TRUE; + astVpssChnAttr[VpssChn].stAspectRatio.u32BgColor = COLOR_RGB_BLACK; + astVpssChnAttr[VpssChn].stNormalize.bEnable = CVI_FALSE; + + crop_w = corp_scale_w < corp_scale_h ? stSizeOut.u32Width * corp_scale_w: stSizeOut.u32Width * corp_scale_h; + crop_h = corp_scale_w < corp_scale_h ? stSizeOut.u32Height * corp_scale_w: stSizeOut.u32Height * corp_scale_h; + if (corp_scale_h < 0 || corp_scale_w < 0) { + SAMPLE_PRT("crop scale error. corp_scale_w: %f, corp_scale_h: %f\n", corp_scale_w, corp_scale_h); + goto error; + } + + stGrpCropInfo.bEnable = true; + stGrpCropInfo.stCropRect.s32X = (stSizeIn.u32Width - crop_w) / 2; + stGrpCropInfo.stCropRect.s32Y = (stSizeIn.u32Height - crop_h) / 2; + stGrpCropInfo.stCropRect.u32Width = crop_w; + stGrpCropInfo.stCropRect.u32Height = crop_h; + } + + /*start vpss*/ + abChnEnable[0] = CVI_TRUE; + s32Ret = SAMPLE_COMM_VPSS_Init(VpssGrp, abChnEnable, &stVpssGrpAttr, astVpssChnAttr); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("init vpss group failed. s32Ret: 0x%x ! retry!!!\n", s32Ret); + s32Ret = SAMPLE_COMM_VPSS_Stop(VpssGrp, abChnEnable); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("stop vpss group failed. s32Ret: 0x%x !\n", s32Ret); + } + s32Ret = SAMPLE_COMM_VPSS_Init(VpssGrp, abChnEnable, &stVpssGrpAttr, astVpssChnAttr); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("retry to init vpss group failed. s32Ret: 0x%x !\n", s32Ret); + return s32Ret; + } else { + SAMPLE_PRT("retry to init vpss group ok!\n"); + } + } + + if (crop_w != 0 && crop_h != 0) { + s32Ret = CVI_VPSS_SetChnCrop(VpssGrp, VpssChn, &stGrpCropInfo); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("set vpss group crop failed. s32Ret: 0x%x !\n", s32Ret); + goto error; + } + } + + s32Ret = SAMPLE_COMM_VPSS_Start(VpssGrp, abChnEnable, &stVpssGrpAttr, astVpssChnAttr); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("start vpss group failed. s32Ret: 0x%x !\n", s32Ret); + goto error; + } + + return s32Ret; +error: + _mmf_vpss_deinit(VpssGrp, VpssChn); + return s32Ret; +} + +static CVI_S32 _mmf_init(void) +{ + MMF_VERSION_S stVersion; + SAMPLE_INI_CFG_S stIniCfg; + SAMPLE_VI_CONFIG_S stViConfig; + + PIC_SIZE_E enPicSize; + SIZE_S stSize; + CVI_S32 s32Ret = CVI_SUCCESS; + LOG_LEVEL_CONF_S log_conf; + + int old_pool_cnt = _get_vb_pool_cnt(); + if (old_pool_cnt > 0) { + if (_is_module_in_use("soph_vi") == 0) { + reinit_soph_vb(); + } else { + printf("You may have repeatedly initialized maix multi-media!"); + } + } + + CVI_SYS_GetVersion(&stVersion); + SAMPLE_PRT("maix multi-media version:%s\n", stVersion.version); + + log_conf.enModId = CVI_ID_LOG; + log_conf.s32Level = CVI_DBG_DEBUG; + CVI_LOG_SetLevelConf(&log_conf); + + // Get config from ini if found. + if (SAMPLE_COMM_VI_ParseIni(&stIniCfg)) { + SAMPLE_PRT("Parse complete\n"); + } + + //Set sensor number + CVI_VI_SetDevNum(stIniCfg.devNum); + + /************************************************ + * step1: Config VI + ************************************************/ + s32Ret = SAMPLE_COMM_VI_IniToViCfg(&stIniCfg, &stViConfig); + if (s32Ret != CVI_SUCCESS) + return s32Ret; + + memcpy(&g_stViConfig, &stViConfig, sizeof(SAMPLE_VI_CONFIG_S)); + memcpy(&g_stIniCfg, &stIniCfg, sizeof(SAMPLE_INI_CFG_S)); + + /************************************************ + * step2: Get input size + ************************************************/ + s32Ret = SAMPLE_COMM_VI_GetSizeBySensor(stIniCfg.enSnsType[0], &enPicSize); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("SAMPLE_COMM_VI_GetSizeBySensor failed with %#x\n", s32Ret); + return s32Ret; + } + + s32Ret = SAMPLE_COMM_SYS_GetPicSize(enPicSize, &stSize); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("SAMPLE_COMM_SYS_GetPicSize failed with %#x\n", s32Ret); + return s32Ret; + } + + /************************************************ + * step3: Init modules + ************************************************/ + if (0 != _free_leak_memory_of_ion()) { + SAMPLE_PRT("free leak ion memory error\n"); + } + + if (0 != _free_leak_memory_of_vb()) { + SAMPLE_PRT("free leak vb memory error\n"); + } + + s32Ret = _mmf_sys_init(stSize); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("sys init failed. s32Ret: 0x%x !\n", s32Ret); + goto _need_exit_sys_and_deinit_vi; + } + + s32Ret = SAMPLE_PLAT_VI_INIT(&stViConfig); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("vi init failed. s32Ret: 0x%x !\n", s32Ret); + SAMPLE_PRT("Please try to check if the camera is working.\n"); + goto _need_exit_sys_and_deinit_vi; + } + + priv.vi_size.u32Width = stSize.u32Width; + priv.vi_size.u32Height = stSize.u32Height; + + return s32Ret; + +_need_exit_sys_and_deinit_vi: + _mmf_sys_exit(); + + return s32Ret; +} + +static void _mmf_deinit(void) +{ + UNUSED(cvi_ive_deinit); + mmf_del_vi_channel_all(); + mmf_del_venc_channel_all(); + mmf_enc_jpg_deinit(0); + _try_release_venc_all(); + _try_release_vpss_all(); + mmf_vi_deinit(); + // mmf_del_region_channel_all(); // need not release + _mmf_sys_exit(); +} + +static int _vi_get_unused_ch() { + for (int i = 0; i < MMF_VI_MAX_CHN; i++) { + if (priv.vi_chn_is_inited[i] == false) { + return i; + } + } + return -1; +} + +int mmf_init(void) +{ + if (priv.mmf_used_cnt) { + priv.mmf_used_cnt ++; + // printf("maix multi-media already inited(cnt:%d)\n", priv.mmf_used_cnt); + return 0; + } + + if (_try_release_sys() != CVI_SUCCESS) { + printf("try release sys failed\n"); + return -1; + } else { + printf("try release sys ok\n"); + } + + if (_mmf_init() != CVI_SUCCESS) { + printf("maix multi-media init failed\n"); + return -1; + } else { + printf("maix multi-media init ok\n"); + } + + UNUSED(cvi_ive_init); + priv.mmf_used_cnt = 1; + + if (_try_release_vi() != CVI_SUCCESS) { + printf("try release vio failed\n"); + return -1; + } else { + printf("try release vio ok\n"); + } + + if (_try_release_venc_all() != CVI_SUCCESS) { + printf("try release venc failed\n"); + return -1; + } else { + printf("try release venc ok\n"); + } + + return 0; +} + +bool mmf_is_init(void) +{ + return priv.mmf_used_cnt > 0 ? true : false; +} + +int mmf_try_deinit(bool force) { + if (!priv.mmf_used_cnt) { + return 0; + } + + if (force) { + priv.mmf_used_cnt = 0; + printf("maix multi-media driver destroyed.\n"); + _mmf_deinit(); + } else { + priv.mmf_used_cnt --; + if (priv.mmf_used_cnt) { + return 0; + } else { + printf("maix multi-media driver destroyed.\n"); + _mmf_deinit(); + } + } + return 0; +} + +int mmf_deinit(void) { + return mmf_try_deinit(false); +} + +int mmf_get_vi_unused_channel(void) { + return _vi_get_unused_ch(); +} + +static CVI_S32 _mmf_vpss_chn_init(VPSS_GRP VpssGrp, VPSS_CHN VpssChn, int width, int height, PIXEL_FORMAT_E format, int fps, int depth, bool mirror, bool flip, int fit) +{ +#if 1 + VPSS_GRP_ATTR_S stGrpAttr; + VPSS_CROP_INFO_S stChnCropInfo; + VPSS_CHN_ATTR_S chn_attr = {0}; + CVI_S32 s32Ret = CVI_SUCCESS; + + s32Ret = CVI_VPSS_GetGrpAttr(VpssGrp, &stGrpAttr); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("CVI_VPSS_GetGrpAttr failed. s32Ret: 0x%x !\n", s32Ret); + return s32Ret; + } + CVI_FLOAT corp_scale_w = (CVI_FLOAT)stGrpAttr.u32MaxW / width; + CVI_FLOAT corp_scale_h = (CVI_FLOAT)stGrpAttr.u32MaxH / height; + CVI_U32 crop_w = -1, crop_h = -1; + if (fit == 0) { + chn_attr.u32Width = width; + chn_attr.u32Height = height; + chn_attr.enVideoFormat = VIDEO_FORMAT_LINEAR; + chn_attr.enPixelFormat = format; + chn_attr.stFrameRate.s32SrcFrameRate = fps; + chn_attr.stFrameRate.s32DstFrameRate = fps; + chn_attr.u32Depth = depth; + chn_attr.bMirror = mirror; + chn_attr.bFlip = flip; + chn_attr.stAspectRatio.enMode = ASPECT_RATIO_MANUAL; + chn_attr.stAspectRatio.stVideoRect.s32X = 0; + chn_attr.stAspectRatio.stVideoRect.s32Y = 0; + chn_attr.stAspectRatio.stVideoRect.u32Width = width; + chn_attr.stAspectRatio.stVideoRect.u32Height = height; + chn_attr.stAspectRatio.bEnableBgColor = CVI_TRUE; + chn_attr.stAspectRatio.u32BgColor = COLOR_RGB_BLACK; + chn_attr.stNormalize.bEnable = CVI_FALSE; + + stChnCropInfo.bEnable = false; + } else if (fit == 1) { + chn_attr.u32Width = width; + chn_attr.u32Height = height; + chn_attr.enVideoFormat = VIDEO_FORMAT_LINEAR; + chn_attr.enPixelFormat = format; + chn_attr.stFrameRate.s32SrcFrameRate = fps; + chn_attr.stFrameRate.s32DstFrameRate = fps; + chn_attr.u32Depth = depth; + chn_attr.bMirror = mirror; + chn_attr.bFlip = flip; + chn_attr.stAspectRatio.enMode = ASPECT_RATIO_AUTO; + chn_attr.stAspectRatio.bEnableBgColor = CVI_TRUE; + chn_attr.stAspectRatio.u32BgColor = COLOR_RGB_BLACK; + chn_attr.stNormalize.bEnable = CVI_FALSE; + + stChnCropInfo.bEnable = false; + } else { + chn_attr.u32Width = width; + chn_attr.u32Height = height; + chn_attr.enVideoFormat = VIDEO_FORMAT_LINEAR; + chn_attr.enPixelFormat = format; + chn_attr.stFrameRate.s32SrcFrameRate = fps; + chn_attr.stFrameRate.s32DstFrameRate = fps; + chn_attr.u32Depth = depth; + chn_attr.bMirror = mirror; + chn_attr.bFlip = flip; + chn_attr.stAspectRatio.enMode = ASPECT_RATIO_AUTO; + chn_attr.stAspectRatio.bEnableBgColor = CVI_TRUE; + chn_attr.stAspectRatio.u32BgColor = COLOR_RGB_BLACK; + chn_attr.stNormalize.bEnable = CVI_FALSE; + + crop_w = corp_scale_w < corp_scale_h ? width * corp_scale_w: width * corp_scale_h; + crop_h = corp_scale_w < corp_scale_h ? height * corp_scale_w: height * corp_scale_h; + if (corp_scale_h < 0 || corp_scale_w < 0) { + SAMPLE_PRT("crop scale error. corp_scale_w: %f, corp_scale_h: %f\n", corp_scale_w, corp_scale_h); + return -1; + } + + stChnCropInfo.bEnable = true; + stChnCropInfo.stCropRect.s32X = (stGrpAttr.u32MaxW - crop_w) / 2; + stChnCropInfo.stCropRect.s32Y = (stGrpAttr.u32MaxH - crop_h) / 2; + stChnCropInfo.stCropRect.u32Width = crop_w; + stChnCropInfo.stCropRect.u32Height = crop_h; + } + + if (crop_w != 0 && crop_h != 0) { + s32Ret = CVI_VPSS_SetChnCrop(VpssGrp, VpssChn, &stChnCropInfo); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("set vpss group crop failed. s32Ret: 0x%x !\n", s32Ret); + return -1; + } + } + + s32Ret = CVI_VPSS_SetChnAttr(VpssGrp, VpssChn, &chn_attr); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("CVI_VPSS_SetChnAttr failed with %#x\n", s32Ret); + return CVI_FAILURE; + } + + s32Ret = CVI_VPSS_EnableChn(VpssGrp, VpssChn); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("CVI_VPSS_EnableChn failed with %#x\n", s32Ret); + return CVI_FAILURE; + } + + return s32Ret; +#else + CVI_S32 s32Ret; + VPSS_CHN_ATTR_S chn_attr = {0}; + chn_attr.u32Width = width; + chn_attr.u32Height = height; + chn_attr.enVideoFormat = VIDEO_FORMAT_LINEAR; + chn_attr.enPixelFormat = format; + chn_attr.stFrameRate.s32SrcFrameRate = fps; + chn_attr.stFrameRate.s32DstFrameRate = fps; + chn_attr.u32Depth = depth; + chn_attr.bMirror = mirror; + chn_attr.bFlip = flip; + chn_attr.stAspectRatio.enMode = ASPECT_RATIO_MANUAL; + chn_attr.stAspectRatio.stVideoRect.s32X = 0; + chn_attr.stAspectRatio.stVideoRect.s32Y = 0; + chn_attr.stAspectRatio.stVideoRect.u32Width = width; + chn_attr.stAspectRatio.stVideoRect.u32Height = height; + chn_attr.stAspectRatio.bEnableBgColor = CVI_TRUE; + chn_attr.stAspectRatio.u32BgColor = COLOR_RGB_BLACK; + chn_attr.stNormalize.bEnable = CVI_FALSE; + + s32Ret = CVI_VPSS_SetChnAttr(VpssGrp, VpssChn, &chn_attr); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("CVI_VPSS_SetChnAttr failed with %#x\n", s32Ret); + return CVI_FAILURE; + } + + s32Ret = CVI_VPSS_EnableChn(VpssGrp, VpssChn); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("CVI_VPSS_EnableChn failed with %#x\n", s32Ret); + return CVI_FAILURE; + } + + return CVI_SUCCESS; +#endif +} + +static CVI_S32 _mmf_vpss_chn_deinit(VPSS_GRP VpssGrp, VPSS_CHN VpssChn) +{ + return CVI_VPSS_DisableChn(VpssGrp, VpssChn); +} + +static CVI_S32 _mmf_vpss_init_new(VPSS_GRP VpssGrp, CVI_U32 width, CVI_U32 height, PIXEL_FORMAT_E format) +{ + VPSS_GRP_ATTR_S stVpssGrpAttr; + CVI_S32 s32Ret = CVI_SUCCESS; + + memset(&stVpssGrpAttr, 0, sizeof(VPSS_GRP_ATTR_S)); + stVpssGrpAttr.stFrameRate.s32SrcFrameRate = -1; + stVpssGrpAttr.stFrameRate.s32DstFrameRate = -1; + stVpssGrpAttr.enPixelFormat = format; + stVpssGrpAttr.u32MaxW = width; + stVpssGrpAttr.u32MaxH = height; + stVpssGrpAttr.u8VpssDev = 0; + + s32Ret = CVI_VPSS_CreateGrp(VpssGrp, &stVpssGrpAttr); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("CVI_VPSS_CreateGrp(grp:%d) retry(%#x)!\n", VpssGrp, s32Ret); + CVI_VPSS_DestroyGrp(VpssGrp); + + s32Ret = CVI_VPSS_CreateGrp(VpssGrp, &stVpssGrpAttr); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("CVI_VPSS_CreateGrp(grp:%d) failed with %#x!\n", VpssGrp, s32Ret); + return CVI_FAILURE; + } + } + + s32Ret = CVI_VPSS_ResetGrp(VpssGrp); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("CVI_VPSS_ResetGrp(grp:%d) failed with %#x!%d\n", VpssGrp, s32Ret, CVI_ERR_VPSS_ILLEGAL_PARAM); + return CVI_FAILURE; + } + + s32Ret = CVI_VPSS_StartGrp(VpssGrp); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("CVI_VPSS_StartGrp failed with %#x\n", s32Ret); + return CVI_FAILURE; + } + return s32Ret; +} + +int mmf_vi_init(void) +{ + if (priv.vi_is_inited) { + return 0; + } + + CVI_S32 s32Ret = CVI_SUCCESS; + s32Ret = _mmf_vpss_init_new(0, priv.vi_size.u32Width, priv.vi_size.u32Height, PIXEL_FORMAT_UYVY); // PIXEL_FORMAT_UYVY PIXEL_FORMAT_NV21 + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("_mmf_vpss_init_new failed. s32Ret: 0x%x !\n", s32Ret); + } + + priv.vi_is_inited = true; + + return s32Ret; +} + +int mmf_vi_deinit(void) +{ + if (!priv.vi_is_inited) { + return 0; + } + + CVI_S32 s32Ret = CVI_SUCCESS; + s32Ret = _mmf_vpss_deinit_new(0); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("_mmf_vpss_deinit_new failed with %#x!\n", s32Ret); + return CVI_FAILURE; + } + + priv.vi_is_inited = false; + + return s32Ret; +} + +static int _mmf_add_vi_channel(int ch, int width, int height, int format) { + uint32_t pool_size_out = 0; + int pool_id = -1; + + if (!priv.mmf_used_cnt) { + printf("%s: maix multi-media or vi not inited\n", __func__); + return -1; + } + + if (!priv.vi_is_inited) { + if (0 != mmf_vi_init()) { + printf("mmf_vi_init failed!\r\n"); + return -1; + } + } + + if (width <= 0 || height <= 0) { + printf("invalid width or height\n"); + return -1; + } + + if (format != PIXEL_FORMAT_NV21 + && format != PIXEL_FORMAT_RGB_888) { + printf("invalid format\n"); + return -1; + } + + // if ((format == PIXEL_FORMAT_RGB_888 && width * height * 3 > 640 * 640 * 3) + // || (format == PIXEL_FORMAT_RGB_888 && width * height * 3 / 2 > 2560 * 1440 * 3 / 2)) { + // printf("camera size is too large, for NV21, maximum resolution 2560x1440, for RGB888, maximum resolution 640x640!\n"); + // return -1; + // } + + if (mmf_vi_chn_is_open(ch)) { + printf("vi ch %d already open\n", ch); + return -1; + } + + CVI_S32 s32Ret = CVI_SUCCESS; + int fps = 30; + int depth = 2; + int width_out = ALIGN(width, DEFAULT_ALIGN); + int height_out = height; + PIXEL_FORMAT_E format_out = (PIXEL_FORMAT_E)format; + bool mirror = !g_priv.vi_hmirror[ch]; + bool flip = !g_priv.vi_vflip[ch]; + s32Ret = _mmf_vpss_chn_deinit(0, ch); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("_mmf_vpss_chn_deinit failed with %#x!\n", s32Ret); + return CVI_FAILURE; + } + + s32Ret = _mmf_vpss_chn_init(0, ch, width_out, height_out, format_out, fps, depth, mirror, flip, 2); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("_mmf_vpss_chn_init failed with %#x!\n", s32Ret); + return CVI_FAILURE; + } + + s32Ret = SAMPLE_COMM_VI_Bind_VPSS(0, ch, 0); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("vi bind vpss failed. s32Ret: 0x%x !\n", s32Ret); + goto _need_deinit_vpss_chn; + } + + char name[20]; + snprintf(name, 20, "vi_vpss%.1d", ch); + pool_size_out = COMMON_GetPicBufferSize(width_out, height_out, format_out, DATA_BITWIDTH_8, COMPRESS_MODE_NONE, DEFAULT_ALIGN); + pool_id = _create_vb_pool(name, MMF_MOD_VI, pool_size_out, 2); + if (pool_id < 0) { + printf("[%s][%d]_create_vb_pool failed, id %d\n", __func__, __LINE__, pool_id); + goto _need_deinit_vpss_chn; + } + + s32Ret = CVI_VPSS_AttachVbPool(0, ch, pool_id); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("CVI_VPSS_AttachVbPool failed. s32Ret: 0x%x !\n", s32Ret); + goto _need_deinit_vpss_chn; + } + + // VIDEO_FRAME_INFO_S frame; + // if ((s32Ret = CVI_VPSS_GetChnFrame(0, ch, &frame, 3000)) != CVI_SUCCESS) { + // SAMPLE_PRT("vi get frame failed: 0x%x !\n", s32Ret); + // if ((s32Ret = SAMPLE_COMM_VI_UnBind_VPSS(0, ch, 0)) != CVI_SUCCESS) { + // SAMPLE_PRT("vi unbind vpss failed. s32Ret: 0x%x !\n", s32Ret); + // } + // goto _need_deinit_vpss_chn; + // } + // CVI_VPSS_ReleaseChnFrame(0, ch, &frame); + + priv.vi_chn_pool_id[ch] = pool_id; + priv.vi_chn_is_inited[ch] = true; + + return 0; +_need_deinit_vpss_chn: + _mmf_vpss_chn_deinit(0, ch); + return -1; +} + +int mmf_add_vi_channel(int ch, int width, int height, int format) { + printf("mmf_add_vi_channel..\r\n"); + return _mmf_add_vi_channel(ch, width, height, format); +} + +int mmf_del_vi_channel(int ch) { + if (ch < 0 || ch >= MMF_VI_MAX_CHN) { + printf("[%d] invalid ch %d\n", __LINE__, ch); + return -1; + } + + if (priv.vi_chn_is_inited[ch] == false) { + return 0; + } + + CVI_S32 s32Ret = CVI_SUCCESS; + s32Ret = SAMPLE_COMM_VI_UnBind_VPSS(0, ch, 0); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("vi unbind vpss failed. s32Ret: 0x%x !\n", s32Ret); + // return -1; // continue to deinit vpss + } + + if (0 != _mmf_vpss_chn_deinit(0, ch)) { + SAMPLE_PRT("_mmf_vpss_chn_deinit failed. s32Ret: 0x%x !\n", s32Ret); + } + + _destroy_vb_pool(priv.vi_chn_pool_id[ch]); + + priv.vi_chn_pool_id[ch] = -1; + priv.vi_chn_is_inited[ch] = false; + return s32Ret; +} + +int mmf_del_vi_channel_all() { + for (int i = 0; i < MMF_VI_MAX_CHN; i++) { + if (priv.vi_chn_is_inited[i] == true) { + mmf_del_vi_channel(i); + } + } + return 0; +} + +bool mmf_vi_chn_is_open(int ch) { + if (ch < 0 || ch >= MMF_VI_MAX_CHN) { + return false; + } + + return priv.vi_chn_is_inited[ch]; +} + +int mmf_reset_vi_channel(int ch, int width, int height, int format) +{ + mmf_del_vi_channel(ch); + return mmf_add_vi_channel(ch, width, height, format); +} + +int mmf_vi_aligned_width(int ch) { + UNUSED(ch); + return DEFAULT_ALIGN; +} + +int mmf_vi_frame_pop(int ch, void **data, int *len, int *width, int *height, int *format) { + if (!priv.vi_chn_is_inited[ch]) { + // printf("vi ch %d not open\n", ch); + return -1; + } + if (ch < 0 || ch >= MMF_VI_MAX_CHN) { + printf("[%d] invalid ch %d\n", __LINE__, ch); + return -1; + } + if (data == NULL || len == NULL || width == NULL || height == NULL || format == NULL) { + printf("invalid param\n"); + return -1; + } + + int ret = -1; + VIDEO_FRAME_INFO_S *frame = &priv.vi_frame[ch]; + if (CVI_VPSS_GetChnFrame(0, ch, frame, 1000) == 0) { + int image_size = frame->stVFrame.u32Length[0] + + frame->stVFrame.u32Length[1] + + frame->stVFrame.u32Length[2]; + CVI_VOID *vir_addr; + vir_addr = CVI_SYS_MmapCache(frame->stVFrame.u64PhyAddr[0], image_size); + CVI_SYS_IonInvalidateCache(frame->stVFrame.u64PhyAddr[0], vir_addr, image_size); + + frame->stVFrame.pu8VirAddr[0] = (CVI_U8 *)vir_addr; // save virtual address for munmap + // printf("width: %d, height: %d, total_buf_length: %d, phy:%#lx vir:%p\n", + // frame->stVFrame.u32Width, + // frame->stVFrame.u32Height, image_size, + // frame->stVFrame.u64PhyAddr[0], vir_addr); + + *data = vir_addr; + *len = image_size; + *width = frame->stVFrame.u32Width; + *height = frame->stVFrame.u32Height; + *format = frame->stVFrame.enPixelFormat; + return 0; + } + return ret; +} + +void mmf_vi_frame_free(int ch) { + VIDEO_FRAME_INFO_S *frame = &priv.vi_frame[ch]; + int image_size = frame->stVFrame.u32Length[0] + + frame->stVFrame.u32Length[1] + + frame->stVFrame.u32Length[2]; + CVI_SYS_Munmap(frame->stVFrame.pu8VirAddr[0], image_size); + if (CVI_VPSS_ReleaseChnFrame(0, ch, frame) != 0) { + SAMPLE_PRT("CVI_VI_ReleaseChnFrame NG\n"); + } +} + +int mmf_region_frame_push(int ch, void *data, int len) +{ + CVI_S32 s32Ret; + RGN_CANVAS_INFO_S stCanvasInfo; + + if (ch < 0 || ch >= MMF_RGN_MAX_NUM) { + SAMPLE_PRT("Handle ch is illegal %d!\n", ch); + return CVI_FAILURE; + } + + if (!priv.rgn_is_init[ch]) { + return 0; + } + + if (!priv.rgn_is_bind[ch]) { + return 0; + } + + s32Ret = CVI_RGN_GetCanvasInfo(ch, &stCanvasInfo); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("CVI_RGN_GetCanvasInfo failed with %#x!\n", s32Ret); + return CVI_FAILURE; + } + + if (stCanvasInfo.enPixelFormat == PIXEL_FORMAT_ARGB_8888) { + if (!data || (CVI_U32)len != stCanvasInfo.stSize.u32Width * stCanvasInfo.stSize.u32Height * 4) { + printf("Param is error!\r\n"); + return CVI_FAILURE; + } + memcpy(stCanvasInfo.pu8VirtAddr, data, len); + } else { + printf("Not support format!\r\n"); + return CVI_FAILURE; + } + + s32Ret = CVI_RGN_UpdateCanvas(ch); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("CVI_RGN_UpdateCanvas failed with %#x!\n", s32Ret); + return CVI_FAILURE; + } + return s32Ret; +} + +int mmf_enc_jpg_init(int ch, int w, int h, int format, int quality) +{ + if (priv.enc_jpg_is_init) + return 0; + + if (quality <= 50) { + printf("quality range is (50, 100]\n"); + return -1; + } + + if (mmf_init()) { + return -1; + } + + // if ((format == PIXEL_FORMAT_RGB_888 && w * h * 3 > 640 * 640 * 3) + // || (format == PIXEL_FORMAT_RGB_888 && w * h * 3 / 2 > 2560 * 1440 * 3 / 2)) { + // printf("image size is too large, for NV21, maximum resolution 2560x1440, for RGB888, maximum resolution 640x640!\n"); + // return -1; + // } + + CVI_S32 s32Ret = CVI_SUCCESS; + + VENC_CHN_ATTR_S stVencChnAttr; + memset(&stVencChnAttr, 0, sizeof(VENC_CHN_ATTR_S)); + stVencChnAttr.stVencAttr.enType = PT_JPEG; + stVencChnAttr.stVencAttr.u32MaxPicWidth = w; + stVencChnAttr.stVencAttr.u32MaxPicHeight = h; + stVencChnAttr.stVencAttr.u32PicWidth = w; + stVencChnAttr.stVencAttr.u32PicHeight = h; + stVencChnAttr.stVencAttr.bEsBufQueueEn = CVI_FALSE; + stVencChnAttr.stVencAttr.bIsoSendFrmEn = CVI_FALSE; + stVencChnAttr.stVencAttr.bByFrame = 1; + stVencChnAttr.stRcAttr.enRcMode = VENC_RC_MODE_MJPEGFIXQP; + + s32Ret = CVI_VENC_CreateChn(ch, &stVencChnAttr); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_CreateChn [%d] failed with %#x\n", ch, s32Ret); + return s32Ret; + } + + s32Ret = CVI_VENC_ResetChn(ch); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_CreateChn [%d] failed with %#x\n", ch, s32Ret); + return s32Ret; + } + + s32Ret = CVI_VENC_DestroyChn(ch); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_destroyChn [%d] failed with %#x\n", ch, s32Ret); + } + + s32Ret = CVI_VENC_CreateChn(ch, &stVencChnAttr); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_CreateChn [%d] failed with %#x\n", ch, s32Ret); + return s32Ret; + } + + VENC_JPEG_PARAM_S stJpegParam; + memset(&stJpegParam, 0, sizeof(VENC_JPEG_PARAM_S)); + s32Ret = CVI_VENC_GetJpegParam(ch, &stJpegParam); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_GetJpegParam failed with %#x\n", s32Ret); + CVI_VENC_DestroyChn(ch); + return s32Ret; + } + stJpegParam.u32Qfactor = quality; + s32Ret = CVI_VENC_SetJpegParam(ch, &stJpegParam); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_SetJpegParam failed with %#x\n", s32Ret); + CVI_VENC_DestroyChn(ch); + return s32Ret; + } + + switch (format) { + case PIXEL_FORMAT_RGB_888: + { + s32Ret = _mmf_vpss_init(2, ch, (SIZE_S){(CVI_U32)w, (CVI_U32)h}, (SIZE_S){(CVI_U32)w, (CVI_U32)h}, PIXEL_FORMAT_RGB_888, PIXEL_FORMAT_YUV_PLANAR_420, 30, 0, CVI_FALSE, CVI_FALSE, 0); + if (s32Ret != CVI_SUCCESS) { + printf("VPSS init failed with %#x\n", s32Ret); + CVI_VENC_StopRecvFrame(ch); + CVI_VENC_DestroyChn(ch); + return s32Ret; + } + + s32Ret = SAMPLE_COMM_VPSS_Bind_VENC(2, ch, ch); + if (s32Ret != CVI_SUCCESS) { + printf("VPSS bind VENC failed with %#x\n", s32Ret); + _mmf_vpss_deinit(2, ch); + CVI_VENC_StopRecvFrame(ch); + CVI_VENC_DestroyChn(ch); + return s32Ret; + } + + uint32_t input_size = 0, output_size = 0; + input_size = COMMON_GetPicBufferSize(w, h, (PIXEL_FORMAT_E)format, DATA_BITWIDTH_8, COMPRESS_MODE_NONE, DEFAULT_ALIGN); + output_size = COMMON_GetPicBufferSize(w, h, (PIXEL_FORMAT_E)PIXEL_FORMAT_YUV_PLANAR_420, DATA_BITWIDTH_8, COMPRESS_MODE_NONE, DEFAULT_ALIGN); + int pool_id = _create_vb_pool((char *)"enc_jpeg_in", MMF_MOD_VENC, input_size, 1); + if (pool_id < 0) { + printf("[%s][%d]_create_vb_pool failed, id %d\n", __func__, __LINE__, pool_id); + return -1; + } + priv.enc_jpg_input_pool_id = pool_id; + + pool_id = _create_vb_pool((char *)"enc_jpeg_out", MMF_MOD_VENC, output_size, 1); + if (pool_id < 0) { + printf("[%s][%d]_create_vb_pool failed, id %d\n", __func__, __LINE__, pool_id); + return -1; + } + priv.enc_jpg_output_pool_id = pool_id; + + s32Ret = CVI_VPSS_AttachVbPool(2, ch, priv.enc_jpg_output_pool_id); + if (s32Ret != CVI_SUCCESS) { + SAMPLE_PRT("CVI_VPSS_AttachVbPool failed. s32Ret: 0x%x !\n", s32Ret); + _destroy_vb_pool(priv.enc_jpg_output_pool_id); + _destroy_vb_pool(priv.enc_jpg_input_pool_id); + return CVI_FAILURE; + } + } + break; + case PIXEL_FORMAT_NV21: + { + uint32_t size = COMMON_GetPicBufferSize(w, h, (PIXEL_FORMAT_E)format, DATA_BITWIDTH_8, COMPRESS_MODE_NONE, DEFAULT_ALIGN); + int pool_id = _create_vb_pool((char *)"enc_jpeg_in", MMF_MOD_VENC, size, 1); + if (pool_id < 0) { + printf("[%s][%d]_create_vb_pool failed, id %d\n", __func__, __LINE__, pool_id); + return -1; + } + priv.enc_jpg_input_pool_id = pool_id; + } + break; + default: + printf("unknown format!\n"); + CVI_VENC_StopRecvFrame(ch); + CVI_VENC_DestroyChn(ch); + return -1; + } + + VENC_RECV_PIC_PARAM_S stRecvParam; + stRecvParam.s32RecvPicNum = -1; + s32Ret = CVI_VENC_StartRecvFrame(ch, &stRecvParam); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_StartRecvPic failed with %#x\n", s32Ret); + return CVI_FAILURE; + } + + if (priv.enc_jpg_frame) { + _mmf_free_frame(priv.enc_jpg_frame); + priv.enc_jpg_frame = NULL; + } + + priv.enc_jpg_frame_w = w; + priv.enc_jpg_frame_h = h; + priv.enc_jpg_frame_fmt = format; + priv.enc_jpg_quality = quality; + priv.enc_jpg_is_init = 1; + priv.enc_jpg_running = 0; + + return s32Ret; +} + +int mmf_enc_jpg_deinit(int ch) +{ + if (!priv.enc_jpg_is_init) + return 0; + + CVI_S32 s32Ret = CVI_SUCCESS; + + if (!mmf_enc_jpg_pop(ch, NULL, NULL)) { + mmf_enc_jpg_free(ch); + } + + switch (priv.enc_jpg_frame_fmt) { + case PIXEL_FORMAT_RGB_888: + s32Ret = SAMPLE_COMM_VPSS_UnBind_VENC(2, ch, ch); + if (s32Ret != CVI_SUCCESS) { + printf("VPSS unbind VENC failed with %d\n", s32Ret); + } + + s32Ret = _mmf_vpss_deinit(2, ch); + if (s32Ret != CVI_SUCCESS) { + printf("VPSS deinit failed with %d\n", s32Ret); + } + break; + case PIXEL_FORMAT_NV21: + break; + default: + break; + } + + s32Ret = CVI_VENC_StopRecvFrame(ch); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_StopRecvPic failed with %d\n", s32Ret); + } + + s32Ret = CVI_VENC_ResetChn(ch); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_ResetChn vechn[%d] failed with %#x!\n", + ch, s32Ret); + } + + s32Ret = CVI_VENC_DestroyChn(ch); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_DestroyChn [%d] failed with %d\n", ch, s32Ret); + } + + if (priv.enc_jpg_frame) { + _mmf_free_frame(priv.enc_jpg_frame); + priv.enc_jpg_frame = NULL; + } + + switch (priv.enc_jpg_frame_fmt) { + case PIXEL_FORMAT_RGB_888: + _destroy_vb_pool(priv.enc_jpg_output_pool_id); + priv.enc_jpg_output_pool_id = -1; + _destroy_vb_pool(priv.enc_jpg_input_pool_id); + priv.enc_jpg_output_pool_id = -1; + break; + case PIXEL_FORMAT_NV21: + _destroy_vb_pool(priv.enc_jpg_input_pool_id); + priv.enc_jpg_output_pool_id = -1; + break; + default: + break; + } + + if (mmf_deinit()) { + return -1; + } + + priv.enc_jpg_frame_w = 0; + priv.enc_jpg_frame_h = 0; + priv.enc_jpg_frame_fmt = 0; + priv.enc_jpg_quality = -1; + priv.enc_jpg_is_init = 0; + priv.enc_jpg_running = 0; + + return s32Ret; +} + +int mmf_enc_jpg_push_with_quality(int ch, uint8_t *data, int w, int h, int format, int quality) +{ + UNUSED(ch); + CVI_S32 s32Ret = CVI_SUCCESS; + if (priv.enc_jpg_running) { + return s32Ret; + } + + int real_format = format; + if (format == PIXEL_FORMAT_UINT8_C1) { + format = PIXEL_FORMAT_NV21; + } + + SIZE_S stSize = {(CVI_U32)w, (CVI_U32)h}; + if (priv.enc_jpg_frame == NULL || priv.enc_jpg_frame_w != w || priv.enc_jpg_frame_h != h || priv.enc_jpg_frame_fmt != format + || priv.enc_jpg_quality != quality) { + mmf_enc_jpg_deinit(ch); + mmf_enc_jpg_init(ch, w, h, format, quality); + priv.enc_jpg_frame_w = w; + priv.enc_jpg_frame_h = h; + priv.enc_jpg_frame_fmt = format; + priv.enc_jpg_quality = quality; + if (priv.enc_jpg_frame) { + _mmf_free_frame(priv.enc_jpg_frame); + priv.enc_jpg_frame = NULL; + } + priv.enc_jpg_frame = (VIDEO_FRAME_INFO_S *)_mmf_alloc_frame(priv.enc_jpg_input_pool_id, stSize, (PIXEL_FORMAT_E)format); + if (!priv.enc_jpg_frame) { + printf("Alloc frame failed!\r\n"); + return -1; + } + } + + switch (real_format) { + case PIXEL_FORMAT_UINT8_C1: + if (priv.enc_jpg_frame->stVFrame.u32Stride[0] != (CVI_U32)w) { + for (int height = 0; height < h; height ++) { + memcpy((uint8_t *)priv.enc_jpg_frame->stVFrame.pu8VirAddr[0] + priv.enc_jpg_frame->stVFrame.u32Stride[0] * height, + ((uint8_t *)data) + w * height, w); + } + + for (int height = priv.enc_jpg_frame->stVFrame.u32Height; height < h / 2; height ++) { + memset((uint8_t *)priv.enc_jpg_frame->stVFrame.pu8VirAddr[0] + priv.enc_jpg_frame->stVFrame.u32Stride[0] * height, + 128, w); + } + } else { + memcpy(priv.enc_jpg_frame->stVFrame.pu8VirAddr[0], ((uint8_t *)data), w * h); + memset(priv.enc_jpg_frame->stVFrame.pu8VirAddr[0] + w * h, 128, w * h / 2); + } + CVI_SYS_IonFlushCache(priv.enc_jpg_frame->stVFrame.u64PhyAddr[0], + priv.enc_jpg_frame->stVFrame.pu8VirAddr[0], + priv.enc_jpg_frame->stVFrame.u32Stride[0] * h * 3 / 2); + s32Ret = CVI_VENC_SendFrame(ch, priv.enc_jpg_frame, 1000); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_SendFrame failed with %d\n", s32Ret); + return s32Ret; + } + break; + case PIXEL_FORMAT_RGB_888: + { + if (priv.enc_jpg_frame->stVFrame.u32Stride[0] != (CVI_U32)w * 3) { + for (CVI_U32 h = 0; h < priv.enc_jpg_frame->stVFrame.u32Height; h++) { + memcpy((uint8_t *)priv.enc_jpg_frame->stVFrame.pu8VirAddr[0] + priv.enc_jpg_frame->stVFrame.u32Stride[0] * h, ((uint8_t *)data) + w * h * 3, w * 3); + } + } else { + memcpy(priv.enc_jpg_frame->stVFrame.pu8VirAddr[0], data, w * h * 3); + } + + s32Ret = CVI_VPSS_SendFrame(2, priv.enc_jpg_frame, 1000); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VPSS_SendFrame failed with %#x\n", s32Ret); + return s32Ret; + } + } + break; + case PIXEL_FORMAT_NV21: + if (priv.enc_jpg_frame->stVFrame.u32Stride[0] != (CVI_U32)w) { + for (CVI_U32 h = 0; h < priv.enc_jpg_frame->stVFrame.u32Height * 3 / 2; h ++) { + memcpy((uint8_t *)priv.enc_jpg_frame->stVFrame.pu8VirAddr[0] + priv.enc_jpg_frame->stVFrame.u32Stride[0] * h, + ((uint8_t *)data) + w * h, w); + } + } else { + memcpy(priv.enc_jpg_frame->stVFrame.pu8VirAddr[0], ((uint8_t *)data), w * h * 3 / 2); + } + + s32Ret = CVI_VENC_SendFrame(ch, priv.enc_jpg_frame, 1000); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_SendFrame failed with %d\n", s32Ret); + return s32Ret; + } + break; + default: return -1; + } + + priv.enc_jpg_running = 1; + + return s32Ret; +} + +int mmf_enc_jpg_push(int ch, uint8_t *data, int w, int h, int format) +{ + UNUSED(ch); + CVI_S32 s32Ret = CVI_SUCCESS; + if (priv.enc_jpg_running) { + return s32Ret; + } + + SIZE_S stSize = {(CVI_U32)w, (CVI_U32)h}; + if (priv.enc_jpg_frame == NULL || priv.enc_jpg_frame_w != w || priv.enc_jpg_frame_h != h || priv.enc_jpg_frame_fmt != format) { + mmf_enc_jpg_deinit(ch); + mmf_enc_jpg_init(ch, w, h, format, 80); + priv.enc_jpg_frame_w = w; + priv.enc_jpg_frame_h = h; + priv.enc_jpg_frame_fmt = format; + if (priv.enc_jpg_frame) { + _mmf_free_frame(priv.enc_jpg_frame); + priv.enc_jpg_frame = NULL; + } + priv.enc_jpg_frame = (VIDEO_FRAME_INFO_S *)_mmf_alloc_frame(priv.enc_jpg_input_pool_id, stSize, (PIXEL_FORMAT_E)format); + if (!priv.enc_jpg_frame) { + printf("Alloc frame failed!\r\n"); + return -1; + } + } + + switch (format) { + case PIXEL_FORMAT_RGB_888: + { + if (priv.enc_jpg_frame->stVFrame.u32Stride[0] != (CVI_U32)w * 3) { + for (CVI_U32 h = 0; h < priv.enc_jpg_frame->stVFrame.u32Height; h++) { + memcpy((uint8_t *)priv.enc_jpg_frame->stVFrame.pu8VirAddr[0] + priv.enc_jpg_frame->stVFrame.u32Stride[0] * h, ((uint8_t *)data) + w * h * 3, w * 3); + } + } else { + memcpy(priv.enc_jpg_frame->stVFrame.pu8VirAddr[0], data, w * h * 3); + } + + s32Ret = CVI_VPSS_SendFrame(2, priv.enc_jpg_frame, 1000); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VPSS_SendFrame failed with %#x\n", s32Ret); + return s32Ret; + } + } + break; + case PIXEL_FORMAT_NV21: + if (priv.enc_jpg_frame->stVFrame.u32Stride[0] != (CVI_U32)w) { + for (CVI_U32 h = 0; h < priv.enc_jpg_frame->stVFrame.u32Height * 3 / 2; h ++) { + memcpy((uint8_t *)priv.enc_jpg_frame->stVFrame.pu8VirAddr[0] + priv.enc_jpg_frame->stVFrame.u32Stride[0] * h, + ((uint8_t *)data) + w * h, w); + } + } else { + memcpy(priv.enc_jpg_frame->stVFrame.pu8VirAddr[0], ((uint8_t *)data), w * h * 3 / 2); + } + + s32Ret = CVI_VENC_SendFrame(ch, priv.enc_jpg_frame, 1000); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_SendFrame failed with %d\n", s32Ret); + return s32Ret; + } + break; + default: return -1; + } + + priv.enc_jpg_running = 1; + + return s32Ret; +} + +int mmf_enc_jpg_pop(int ch, uint8_t **data, int *size) +{ + CVI_S32 s32Ret = CVI_SUCCESS; + if (!priv.enc_jpg_running) { + return s32Ret; + } + + priv.enc_jpeg_frame.pstPack = (VENC_PACK_S *)malloc(sizeof(VENC_PACK_S) * 1); + if (!priv.enc_jpeg_frame.pstPack) { + printf("Malloc failed!\r\n"); + return -1; + } + + VENC_CHN_STATUS_S stStatus; + s32Ret = CVI_VENC_QueryStatus(ch, &stStatus); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_QueryStatus failed with %#x\n", s32Ret); + return s32Ret; + } + + if (stStatus.u32CurPacks > 0) { + s32Ret = CVI_VENC_GetStream(ch, &priv.enc_jpeg_frame, 1000); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_GetStream failed with %#x\n", s32Ret); + return s32Ret; + } + } else { + printf("CVI_VENC_QueryStatus find not pack\r\n"); + return -1; + } + + if (data) + *data = priv.enc_jpeg_frame.pstPack[0].pu8Addr; + if (size) + *size = priv.enc_jpeg_frame.pstPack[0].u32Len; + + return s32Ret; +} + +int mmf_enc_jpg_free(int ch) +{ + CVI_S32 s32Ret = CVI_SUCCESS; + if (!priv.enc_jpg_running) { + return s32Ret; + } + + s32Ret = CVI_VENC_ReleaseStream(ch, &priv.enc_jpeg_frame); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_ReleaseStream failed with %#x\n", s32Ret); + return s32Ret; + } + + if (priv.enc_jpeg_frame.pstPack) { + free(priv.enc_jpeg_frame.pstPack); + priv.enc_jpeg_frame.pstPack = NULL; + } + + priv.enc_jpg_running = 0; + return s32Ret; +} + +int mmf_invert_format_to_mmf(int maix_format) { + switch (maix_format) { + case 0: + return PIXEL_FORMAT_RGB_888; + case 1: + return PIXEL_FORMAT_BGR_888; + case 3: + return PIXEL_FORMAT_ARGB_8888; + case 8: + return PIXEL_FORMAT_NV21; + case 12: + return PIXEL_FORMAT_UINT8_C1; + default: + return -1; + } +} + +void mmf_set_vi_hmirror(int ch, bool en) +{ + if (ch > MMF_VI_MAX_CHN) { + printf("invalid ch, must be [0, %d)\r\n", ch); + return; + } + + g_priv.vi_hmirror[ch] = en; +} + +void mmf_get_vi_hmirror(int ch, bool *en) +{ + if (ch > MMF_VI_MAX_CHN) { + printf("invalid ch, must be [0, %d)\r\n", ch); + return; + } + + *en = (bool)g_priv.vi_hmirror[ch]; +} + +void mmf_set_vi_vflip(int ch, bool en) +{ + if (ch > MMF_VI_MAX_CHN) { + printf("invalid ch, must be [0, %d)\r\n", ch); + return; + } + + g_priv.vi_vflip[ch] = en; +} + +void mmf_get_vi_vflip(int ch, bool *en) +{ + if (ch > MMF_VI_MAX_CHN) { + printf("invalid ch, must be [0, %d)\r\n", ch); + return; + } + + *en = (bool)g_priv.vi_vflip[ch]; +} + +int mmf_add_venc_channel(int ch, mmf_venc_cfg_t *cfg) { + CVI_S32 s32Ret = CVI_SUCCESS; + if (ch >= MMF_VENC_MAX_CHN || priv.venc[ch].is_used) { + printf("Invalid venc ch:%d\r\n", ch); + return -1; + } + + switch (cfg->type) { + case 2: + { + VENC_CHN_ATTR_S stVencChnAttr; + memset(&stVencChnAttr, 0, sizeof(VENC_CHN_ATTR_S)); + stVencChnAttr.stVencAttr.enType = PT_H264; + stVencChnAttr.stVencAttr.u32MaxPicWidth = cfg->w; + stVencChnAttr.stVencAttr.u32MaxPicHeight = cfg->h; + stVencChnAttr.stVencAttr.u32BufSize = 1024 * 1024; // 1024Kb + stVencChnAttr.stVencAttr.bByFrame = CVI_TRUE; + stVencChnAttr.stVencAttr.u32PicWidth = cfg->w; + stVencChnAttr.stVencAttr.u32PicHeight = cfg->h; + stVencChnAttr.stVencAttr.bEsBufQueueEn = CVI_TRUE; + stVencChnAttr.stVencAttr.bIsoSendFrmEn = CVI_TRUE; + stVencChnAttr.stGopAttr.enGopMode = VENC_GOPMODE_NORMALP; + stVencChnAttr.stGopAttr.stNormalP.s32IPQpDelta = 2; + stVencChnAttr.stRcAttr.enRcMode = VENC_RC_MODE_H264CBR; + stVencChnAttr.stRcAttr.stH264Cbr.u32Gop = cfg->gop; + stVencChnAttr.stRcAttr.stH264Cbr.u32StatTime = 2; + stVencChnAttr.stRcAttr.stH264Cbr.u32SrcFrameRate = cfg->intput_fps; + stVencChnAttr.stRcAttr.stH264Cbr.fr32DstFrameRate = cfg->output_fps; + stVencChnAttr.stRcAttr.stH264Cbr.u32BitRate = cfg->bitrate; + stVencChnAttr.stRcAttr.stH264Cbr.bVariFpsEn = 0; + s32Ret = CVI_VENC_CreateChn(ch, &stVencChnAttr); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_CreateChn [%d] failed with %d\n", ch, s32Ret); + return s32Ret; + } + + VENC_RECV_PIC_PARAM_S stRecvParam; + stRecvParam.s32RecvPicNum = -1; + s32Ret = CVI_VENC_StartRecvFrame(ch, &stRecvParam); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_StartRecvPic failed with %d\n", s32Ret); + return CVI_FAILURE; + } + + VENC_RC_PARAM_S stRcParam; + s32Ret = CVI_VENC_GetRcParam(ch, &stRcParam); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_GetRcParam failed with %d\n", s32Ret); + return s32Ret; + } + stRcParam.s32FirstFrameStartQp = 35; + stRcParam.s32InitialDelay = 1000; + stRcParam.stParamH264Cbr.u32MinIprop = 1; + stRcParam.stParamH264Cbr.u32MaxIprop = 10; + stRcParam.stParamH264Cbr.u32MaxQp = 51; + stRcParam.stParamH264Cbr.u32MinQp = 20; + stRcParam.stParamH264Cbr.u32MaxIQp = 51; + stRcParam.stParamH264Cbr.u32MinIQp = 20; + s32Ret = CVI_VENC_SetRcParam(ch, &stRcParam); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_SetRcParam failed with %d\n", s32Ret); + return s32Ret; + } + + VENC_FRAMELOST_S stFL; + s32Ret = CVI_VENC_GetFrameLostStrategy(ch, &stFL); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_GetFrameLostStrategy failed with %d\n", s32Ret); + return s32Ret; + } + stFL.enFrmLostMode = FRMLOST_PSKIP; + s32Ret = CVI_VENC_SetFrameLostStrategy(ch, &stFL); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_SetFrameLostStrategy failed with %d\n", s32Ret); + return s32Ret; + } + + break; + } + default: printf("Only support h264 encode! type:%d\r\n", cfg->type); + return -1; + } + + char name[20]; + snprintf(name, 20, "venc%.1d", ch); + uint32_t size = VDEC_GetPicBufferSize((PAYLOAD_TYPE_E)cfg->type, cfg->w, cfg->h, (PIXEL_FORMAT_E)cfg->fmt, DATA_BITWIDTH_8, COMPRESS_MODE_NONE); + int pool_id = _create_vb_pool(name, MMF_MOD_VENC, size, 1); + if (pool_id < 0) { + printf("[%s][%d]_create_vb_pool failed, id %d\n", __func__, __LINE__, pool_id); + return CVI_FAILURE; + } + + venc_info_t *info = (venc_info_t *)&priv.venc[ch]; + info->ch = ch; + info->type = cfg->type; + info->pool_id = pool_id; + memcpy(&info->cfg, cfg, sizeof(mmf_venc_cfg_t)); + info->capture_frame = (VIDEO_FRAME_INFO_S *)_mmf_alloc_frame(info->pool_id, (SIZE_S){(CVI_U32)cfg->w, (CVI_U32)cfg->h}, (PIXEL_FORMAT_E)cfg->fmt); + if (!info->capture_frame) { + printf("Alloc frame failed!\r\n"); + CVI_VENC_DestroyChn(ch); + _destroy_vb_pool(pool_id); + return -1; + } + info->is_used = 1; + info->is_inited = 1; + priv.h265_or_h264_is_used = 1; + + return 0; +} + +int mmf_del_venc_channel(int ch) { + if (!priv.venc[ch].is_inited) { + return 0; + } + + mmf_stream_t stream; + if (!mmf_venc_pop(ch, &stream)) { + mmf_venc_free(ch); + } + + CVI_S32 s32Ret = CVI_SUCCESS; + s32Ret = CVI_VENC_StopRecvFrame(ch); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_StopRecvPic failed with %d\n", s32Ret); + } + + s32Ret = CVI_VENC_ResetChn(ch); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_ResetChn vechn[%d] failed with %#x!\n", ch, s32Ret); + } + + s32Ret = CVI_VENC_DestroyChn(ch); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_DestroyChn [%d] failed with %d\n", ch, s32Ret); + } + + if (priv.venc[ch].capture_frame) { + _mmf_free_frame(priv.venc[ch].capture_frame); + priv.venc[ch].capture_frame = NULL; + } + + _destroy_vb_pool(priv.venc[ch].pool_id); + + if (priv.venc[ch].type == 2 || priv.venc[ch].type == 1) { + priv.h265_or_h264_is_used = 0; + } + priv.venc[ch].is_inited = 0; + priv.venc[ch].is_used = 0; + + return 0; +} + +int mmf_del_venc_channel_all() { + for (int i = 0; i < MMF_VENC_MAX_CHN; i ++) { + mmf_del_venc_channel(i); + } + return 0; +} + +int mmf_venc_push(int ch, uint8_t *data, int w, int h, int format) { + int res = 0; + CVI_S32 s32Ret = CVI_SUCCESS; + if (ch >= MMF_VENC_MAX_CHN || data == NULL + || (format != PIXEL_FORMAT_NV21 && format != PIXEL_FORMAT_RGB_888)) { + printf("Invalid param. ch:%d data:%p format:%d\r\n", ch, data, format); + return -1; + } + + venc_info_t *info = (venc_info_t *)&priv.venc[ch]; + VIDEO_FRAME_INFO_S *frame_info = (VIDEO_FRAME_INFO_S *)info->capture_frame; + if (frame_info == NULL) { + printf("frame info is null!\r\n"); + return -1; + } + + switch (format) { + case PIXEL_FORMAT_NV21: + { + if (frame_info->stVFrame.u32Stride[0] != (CVI_U32)w) { + for (int h0 = 0; h0 < h * 3 / 2; h0 ++) { + memcpy((uint8_t *)frame_info->stVFrame.pu8VirAddr[0] + frame_info->stVFrame.u32Stride[0] * h, + ((uint8_t *)data) + w * h0, w); + } + } else { + memcpy((uint8_t *)frame_info->stVFrame.pu8VirAddr[0], ((uint8_t *)data), w * h * 3 / 2); + } + } + break; + default: + printf("Not support format:%d\r\n", format); + return -1; + } + + s32Ret = CVI_VENC_SendFrame(ch, frame_info, 1000); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_SendFrame failed with %#x\n", s32Ret); + return s32Ret; + } + + info->is_running = 1; + + return res; +} + +int mmf_venc_pop(int ch, mmf_stream_t *stream) { + CVI_S32 s32Ret = CVI_SUCCESS; + if (ch >= MMF_VENC_MAX_CHN || !priv.venc[ch].is_inited) { + printf("Invalid venc ch:%d\r\n", ch); + return -1; + } + + venc_info_t *info = (venc_info_t *)&priv.venc[ch]; + VENC_STREAM_S *venc_stream = (VENC_STREAM_S *)&priv.venc[ch].capture_stream; + if (!info->is_running) { + return s32Ret; + } + + int fd = CVI_VENC_GetFd(ch); + if (fd < 0) { + printf("CVI_VENC_GetFd failed with %d\n", fd); + return -1; + } + + fd_set readFds; + struct timeval timeoutVal; + FD_ZERO(&readFds); + FD_SET(fd, &readFds); + timeoutVal.tv_sec = 0; + timeoutVal.tv_usec = 80*1000; + s32Ret = select(fd + 1, &readFds, NULL, NULL, &timeoutVal); + if (s32Ret < 0) { + if (errno == EINTR) { + printf("VencChn(%d) select failed!\n", ch); + return -1; + } + } else if (s32Ret == 0) { + printf("VencChn(%d) select timeout!\n", ch); + return -1; + } + + venc_stream->pstPack = (VENC_PACK_S *)malloc(sizeof(VENC_PACK_S) * 8); + if (!venc_stream->pstPack) { + printf("Malloc failed!\r\n"); + return -1; + } + + + VENC_CHN_STATUS_S stStatus; + s32Ret = CVI_VENC_QueryStatus(ch, &stStatus); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_QueryStatus failed with %#x\n", s32Ret); + return s32Ret; + } + + if (stStatus.u32CurPacks > 0) { + s32Ret = CVI_VENC_GetStream(ch, venc_stream, 1000); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_GetStream failed with %#x\n", s32Ret); + free(venc_stream->pstPack); + return s32Ret; + } + } else { + printf("CVI_VENC_QueryStatus find not pack\r\n"); + free(venc_stream->pstPack); + return -1; + } + + if (stream) { + stream->count = venc_stream->u32PackCount; + if (stream->count > 8) { + printf("pack count is too large! cnt:%d\r\n", stream->count); + free(venc_stream->pstPack); + return -1; + } + for (int i = 0; i < stream->count; i++) { + stream->data[i] = venc_stream->pstPack[i].pu8Addr + venc_stream->pstPack[i].u32Offset; + stream->data_size[i] = venc_stream->pstPack[i].u32Len - venc_stream->pstPack[i].u32Offset; + } + } + + return 0; +} + +int mmf_venc_free(int ch) { + CVI_S32 s32Ret = CVI_SUCCESS; + if (ch >= MMF_VENC_MAX_CHN || !priv.venc[ch].is_inited) { + printf("Invalid venc ch:%d\r\n", ch); + return -1; + } + + venc_info_t *info = (venc_info_t *)&priv.venc[ch]; + VENC_STREAM_S *venc_stream = (VENC_STREAM_S *)&priv.venc[ch].capture_stream; + if (!info->is_running) { + return s32Ret; + } + + s32Ret = CVI_VENC_ReleaseStream(ch, venc_stream); + if (s32Ret != CVI_SUCCESS) { + printf("CVI_VENC_ReleaseStream failed with %#x\n", s32Ret); + return s32Ret; + } + + if (venc_stream->pstPack) { + free(venc_stream->pstPack); + venc_stream->pstPack = NULL; + } + + info->is_running = 0; + return s32Ret; +} + + + + \ No newline at end of file diff --git a/vision/components/vision/CMakeLists.txt b/vision/components/vision/CMakeLists.txt new file mode 100644 index 0000000..c683409 --- /dev/null +++ b/vision/components/vision/CMakeLists.txt @@ -0,0 +1,77 @@ +# Config enable component2 or not in Kconfig +################# Add include ################# +list(APPEND ADD_INCLUDE "include" + "include/base" +) +if(PLATFORM_LINUX) + list(APPEND ADD_PRIVATE_INCLUDE "port/linux") +elseif(PLATFORM_MAIXCAM) + list(APPEND ADD_PRIVATE_INCLUDE "port/maixcam") +endif() +list(APPEND ADD_PRIVATE_INCLUDE "include_private") +############################################### + +############## Add source files ############### +# list(APPEND ADD_SRCS "src/lib2.c" +# ) +# FILE(GLOB_RECURSE EXTRA_SRC "src/*.c") +# FILE(GLOB EXTRA_SRC "src/*.c") +# list(APPEND ADD_SRCS ${EXTRA_SRC}) +# aux_source_directory(src ADD_SRCS) # collect all source file in src dir, will set var ADD_SRCS +append_srcs_dir(ADD_SRCS "src") # append source file in src dir to var ADD_SRCS +if(PLATFORM_LINUX) + append_srcs_dir(ADD_SRCS "port/linux") +elseif(PLATFORM_MAIXCAM) + append_srcs_dir(ADD_SRCS "port/maixcam") +endif() +# list(REMOVE_ITEM COMPONENT_SRCS "src/test.c") +# set(ADD_ASM_SRCS "src/asm.S") +# list(APPEND ADD_SRCS ${ADD_ASM_SRCS}) +# SET_PROPERTY(SOURCE ${ADD_ASM_SRCS} PROPERTY LANGUAGE C) # set .S ASM file as C language +# SET_SOURCE_FILES_PROPERTIES(${ADD_ASM_SRCS} PROPERTIES COMPILE_FLAGS "-x assembler-with-cpp -D BBBBB") +############################################### + + +###### Add required/dependent components ###### +list(APPEND ADD_REQUIREMENTS basic opencv opencv_freetype websocket peripheral) +list(APPEND ADD_REQUIREMENTS omv) +list(APPEND ADD_REQUIREMENTS kvm_mmf) +############################################### + +###### Add link search path for requirements/libs ###### +# list(APPEND ADD_LINK_SEARCH_PATH "${CONFIG_TOOLCHAIN_PATH}/lib") +# list(APPEND ADD_REQUIREMENTS pthread m) # add system libs, pthread and math lib for example here +# set (OpenCV_DIR opencv/lib/cmake/opencv4) +# find_package(OpenCV REQUIRED) +############################################### + +############ Add static libs ################## +# list(APPEND ADD_STATIC_LIB "lib/libtest.a") +############################################### + +############ Add dynamic libs ################## +# list(APPEND ADD_DYNAMIC_LIB "lib/arch/v831/libmaix_nn.so" +# "lib/arch/v831/libmaix_cam.so" +# ) +############################################### + +#### Add compile option for this component #### +#### Just for this component, won't affect other +#### modules, including component that depend +#### on this component +# list(APPEND ADD_DEFINITIONS_PRIVATE -DAAAAA=1) + +#### Add compile option for this component +#### and components denpend on this component +# list(APPEND ADD_DEFINITIONS -DAAAAA222=1 +# -DAAAAA333=1) +############################################### + +############ Add static libs ################## +#### Update parent's variables like CMAKE_C_LINK_FLAGS +# set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--start-group libmaix/libtest.a -ltest2 -Wl,--end-group" PARENT_SCOPE) +############################################### + +# register component, DYNAMIC or SHARED flags will make component compiled to dynamic(shared) lib +register_component() + diff --git a/vision/components/vision/Kconfig b/vision/components/vision/Kconfig new file mode 100644 index 0000000..e69de29 diff --git a/vision/components/vision/include/base/maix_camera_base.hpp b/vision/components/vision/include/base/maix_camera_base.hpp new file mode 100644 index 0000000..2e4eb79 --- /dev/null +++ b/vision/components/vision/include/base/maix_camera_base.hpp @@ -0,0 +1,109 @@ +/** + * @file maix_camera_base.hpp + * @brief Maix camera SDL implementation + * @author neucrack@sipeed.com + * @license Apache 2.0 Sipeed Ltd + * @update date 2023-10-23 Create by neucrack +*/ + +#pragma once + +#include +#include "maix_image.hpp" +#include "maix_err.hpp" + +namespace maix::camera +{ + class CameraBase + { + public: + /** + * @brief Construct a new Camera object + * @param device camera device name, you can get devices by list_devices method, by default(value is NULL(None in MaixPy)) means the first device + * @param width camera width, by default(value is -1) means auto detect, + * if width > max device supported width, will auto set to max device supported width + * @param height camera height, by default(value is -1) means auto detect, + * if height > max device supported height, will auto set to max device supported height + * @param format camera format, by default(value is FMT_RGB888) + * @param buff_num camera buffer number, by default(value is 3) + */ + CameraBase(const char *device = nullptr, int width = -1, int height = -1, image::Format format = image::Format::FMT_RGB888, int buff_num = 3){}; + + /** + * @brief Judge if the given format is supported by the camera + */ + virtual bool is_support_format(image::Format format) = 0; + + /** + * @brief open camera device + * @param width camera width, by default(value is -1) means auto detect, + * if width > max device supported width, will auto set to max device supported width + * @param height camera height, by default(value is -1) means auto detect, + * if height > max device supported height, will auto set to max device supported height + * @param format camera format, by default(value is FMT_RGB888) + * @param buff_num camera buffer number, by default(value is 3) + * @return error code + */ + virtual err::Err open(int width = -1, int height = -1, image::Format format = image::Format::FMT_RGB888, int buff_num = 3) = 0; + + /** + * @brief read a frame from camera + * @param buff buffer to store image data, if NULL, will alloc a new buffer + * @return image data + */ + virtual image::Image *read(void *buff = NULL, size_t buff_size = 0) = 0; + + /** + * @brief close camera device + * @return none + */ + virtual void close() = 0; + + /** + * Add a new channel and return a new Camera object, you can use close() to close this channel. + * @param width camera width, default is -1, means auto, mostly means max width of camera support + * @param height camera height, default is -1, means auto, mostly means max height of camera support + * @param format camera output format, default is RGB888 + * @param buff_num camera buffer number, default is 3, means 3 buffer, one used by user, one used for cache the next frame, + * more than one buffer will accelerate image read speed, but will cost more memory. + * @return new Camera object + */ + virtual camera::CameraBase *add_channel(int width = -1, int height = -1, image::Format format = image::FMT_RGB888, int buff_num = 3) = 0; + + /** + * @brief clear all buffer + * @return none + */ + virtual void clear_buff() = 0; + + /** + * @brief check camera device is opened or not + * @return opened or not, bool type + */ + virtual bool is_opened() = 0; + + /** + * Get camera supported channels(layers) + */ + virtual int get_ch_nums() = 0; + + /** + * Get channel number of camera. + */ + virtual int get_channel() = 0; + + /** + * Set/Get camera mirror + * @param en enable/disable mirror + */ + virtual int hmirror(int en) = 0; + + /** + * Set/Get camera flip + * @param en enable/disable flip + */ + virtual int vflip(int en) = 0; + }; + +} + diff --git a/vision/components/vision/include/maix_camera.hpp b/vision/components/vision/include/maix_camera.hpp new file mode 100644 index 0000000..ef15de3 --- /dev/null +++ b/vision/components/vision/include/maix_camera.hpp @@ -0,0 +1,305 @@ +/** + * @author neucrack@sipeed, lxowalle@sipeed + * @copyright Sipeed Ltd 2023- + * @license Apache 2.0 + * @update 2023.9.8: Add framework, create this file. + */ + +#pragma once + +#include "maix_tensor.hpp" +#include "maix_log.hpp" +#include "maix_image.hpp" +#include "maix_err.hpp" +#include "maix_camera_base.hpp" +#include +#include +#include +#include +#include + +/** + * @brief maix.camera module, access camera device and get image from it + * @maixpy maix.camera +*/ +namespace maix::camera +{ + /** + * List all supported camera devices. + * @return Returns the path to the camera device. + * @maixpy maix.camera.list_devices + */ + std::vector list_devices(); + + /** + * Enable set camera registers, default is false, if set to true, will not set camera registers, you can manually set registers by write_reg API. + * @param enable enable/disable set camera registers + * @maixpy maix.camera.set_regs_enable + */ + void set_regs_enable(bool enable = true); + + /** + * Camera class + * @maixpy maix.camera.Camera + */ + class Camera + { + public: + /** + * @brief Construct a new Camera object. + * Maximum resolution support 2560x1440. + * @param width camera width, default is -1, means auto, mostly means max width of camera support + * @param height camera height, default is -1, means auto, mostly means max height of camera support + * @param format camera output format, default is image.Format.FMT_RGB888 + * @param device camera device path, you can get devices by list_devices method, by default(value is NULL(None in MaixPy)) means the first device + * @param fps camera fps, default is -1, means auto, mostly means max fps of camera support + * @param buff_num camera buffer number, default is 3, means 3 buffer, one used by user, one used for cache the next frame, + * more than one buffer will accelerate image read speed, but will cost more memory. + * @param open If true, camera will automatically call open() after creation. default is true. + * @maixpy maix.camera.Camera.__init__ + * @maixcdk maix.camera.Camera.Camera + */ + Camera(int width = -1, int height = -1, image::Format format = image::FMT_RGB888, const char *device = nullptr, int fps = -1, int buff_num = 3, bool open = true); + + /** + * @brief Construct a new Camera object. + * @attention CameraBase * parameter need to be set manually, otherwise the operation of this object will be invalid. + * @param device camera device path, you can get devices by list_devices method, by default(value is NULL(None in MaixPy)) means the first device + * @param base basic operation objects. + * @param width camera width, default is -1, means auto, mostly means max width of camera support + * @param height camera height, default is -1, means auto, mostly means max height of camera support + * @param format camera output format, default is image.Format.FMT_RGB888 + * @param fps camera fps, default is -1, means auto, mostly means max fps of camera support + * @param buff_num camera buffer number, default is 3, means 3 buffer, one used by user, one used for cache the next frame, + * more than one buffer will accelerate image read speed, but will cost more memory. + * @param open If true, camera will automatically call open() after creation. default is true. + * @maixcdk maix.camera.Camera.Camera + */ + Camera(const char *device, CameraBase *base, int width = -1, int height = -1, image::Format format = image::FMT_RGB888, int fps = -1, int buff_num = -1, bool open = true); + + ~Camera(); + + void restart(int width = -1, int height = -1, image::Format format = image::FMT_RGB888, const char *device = nullptr, int fps = -1, int buff_num = 3, bool open = true); + + /** + * Get the number of channels supported by the camera. + * @return Returns the maximum number of channels. + * @maixpy maix.camera.Camera.get_ch_nums + */ + int get_ch_nums(); + + /** + * Open camera and run + * @param width camera width, default is -1, means auto, mostly means max width of camera support + * @param height camera height, default is -1, means auto, mostly means max height of camera support + * @param format camera output format, default same as the constructor's format argument + * @param fps camera fps, default is -1, means auto, mostly means max fps of camera support + * @param buff_num camera buffer number, default is 3, means 3 buffer, one used by user, one used for cache the next frame, + * more than one buffer will accelerate image read speed, but will cost more memory. + * @return error code, err::ERR_NONE means success, others means failed + * @maixpy maix.camera.Camera.open + */ + err::Err open(int width = -1, int height = -1, image::Format format = image::FMT_INVALID, int fps = -1, int buff_num = -1); + + /** + * Get one frame image from camera buffer, must call open method before read. + * If open method not called, will call it automatically, if open failed, will throw exception! + * So call open method before read is recommended. + * @param buff buffer to store image data, if buff is nullptr, will alloc memory automatically. + * In MaixPy, default to None, you can create a image.Image object, then pass img.data() to buff. + * @param block block read, default is true, means block util read image successfully, + * if set to false, will return nullptr if no image in buffer + * @return image::Image object, if failed, return nullptr, you should delete if manually in C++ + * @maixpy maix.camera.Camera.read + */ + image::Image *read(void *buff = nullptr, size_t buff_size = 0, bool block = true); + + /** + * Clear buff to ensure the next read image is the latest image + * @maixpy maix.camera.Camera.clear_buff + */ + void clear_buff(); + + /** + * Read some frames and drop, this is usually used avoid read not stable image when camera just opened. + * @param num number of frames to read and drop + * @maixpy maix.camera.Camera.skip_frames + */ + void skip_frames(int num); + + /** + * Close camera + * @maixpy maix.camera.Camera.close + */ + void close(); + + /** + * Add a new channel and return a new Camera object, you can use close() to close this channel. + * @param width camera width, default is -1, means auto, mostly means max width of camera support + * @param height camera height, default is -1, means auto, mostly means max height of camera support + * @param format camera output format, default is RGB888 + * @param fps camera fps, default is -1, means auto, mostly means max fps of camera support + * @param buff_num camera buffer number, default is 3, means 3 buffer, one used by user, one used for cache the next frame, + * more than one buffer will accelerate image read speed, but will cost more memory. + * @param open If true, camera will automatically call open() after creation. default is true. + * @return new Camera object + * @maixpy maix.camera.Camera.add_channel + */ + camera::Camera *add_channel(int width = -1, int height = -1, image::Format format = image::FMT_RGB888, int fps = -1, int buff_num = 3, bool open = true); + + /** + * Check if camera is opened + * @return true if camera is opened, false if not + * @maixpy maix.camera.Camera.is_opened + */ + bool is_opened(); + + /** + * @brief check camera device is closed or not + * @return closed or not, bool type + * @maixpy maix.camera.Camera.is_closed + */ + bool is_closed() { return !is_opened();} + + /** + * Get camera width + * @return camera width + * @maixpy maix.camera.Camera.width + */ + int width() + { + return _width; + } + + /** + * Get camera height + * @return camera height + * @maixpy maix.camera.Camera.height + */ + int height() + { + return _height; + } + + /** + * Get camera fps + * @return camera fps + * @maixpy maix.camera.Camera.fps + */ + int fps() + { + return _fps; + } + + /** + * Get camera output format + * @return camera output format, image::Format object + * @maixpy maix.camera.Camera.format + */ + image::Format format() + { + return _format; + } + + /** + * Get camera buffer number + * @return camera buffer number + * @maixpy maix.camera.Camera.buff_num + */ + int buff_num() + { + return _buff_num; + } + + /** + * Set/Get camera horizontal mirror + * @return camera horizontal mirror + * @maixpy maix.camera.Camera.hmirror + */ + int hmirror(int value = -1); + + /** + * Set/Get camera vertical flip + * @return camera vertical flip + * @maixpy maix.camera.Camera.vflip + */ + int vflip(int value = -1); + + /** + * Get camera device path + * @return camera device path + * @maixpy maix.camera.Camera.device + */ + std::string device() + { + return _device; + } + + /** + * Write camera register + * @param addr register address + * @param data register data + * @param bit_width register data bit width, default is 8 + * @return error code, err::ERR_NONE means success, others means failed + * @maixpy maix.camera.Camera.write_reg + */ + err::Err write_reg(int addr, int data, int bit_width = 8) + { + return err::ERR_NONE; + } + + /** + * Read camera register + * @param addr register address + * @return register data, -1 means failed + * @param bit_width register data bit width, default is 8 + * @maixpy maix.camera.Camera.read_reg + */ + int read_reg(int addr, int bit_width = 8) + { + return -1; + } + + /** + * Camera output color bar image for test + * @param enable enable/disable color bar + * @return error code, err::ERR_NONE means success, others means failed + * @maixpy maix.camera.Camera.show_colorbar + */ + err::Err show_colorbar(bool enable); + + /** + * Get channel of camera + * @return channel number + * @maixpy maix.camera.Camera.get_channel + */ + int get_channel(); + + /** + * Set camera resolution + * @param width new width + * @param height new height + * @return error code, err::ERR_NONE means success, others means failed + * @maixpy maix.camera.Camera.set_resolution + */ + err::Err set_resolution(int width, int height); + + private: + std::string _device; + int _ch; + int _width; + int _height; + int _fps; + int _buff_num; + image::Format _format; + image::Format _format_impl; // used by implement code and need convert to _format + int _hmirror; + int _vflip; + float _exposure; + float _gain; + bool _show_colorbar; + bool _open_set_regs; + CameraBase *_impl; // used by implement code + bool _check_format(image::Format format); + }; +} diff --git a/vision/components/vision/include/maix_image.hpp b/vision/components/vision/include/maix_image.hpp new file mode 100644 index 0000000..7422bfe --- /dev/null +++ b/vision/components/vision/include/maix_image.hpp @@ -0,0 +1,1540 @@ +/** + * @author neucrack@sipeed, lxowalle@sipeed + * @copyright Sipeed Ltd 2023- + * @license Apache 2.0 + * @update 2023.9.8: Add framework, create this file. + */ + +#pragma once + +#include "maix_tensor.hpp" +#include "maix_log.hpp" +#include "maix_err.hpp" +#include "maix_fs.hpp" +#include "maix_image_def.hpp" +#include "maix_image_color.hpp" +#include "maix_image_obj.hpp" +#include "maix_type.hpp" +#include + +/** + * @brief maix.image module, image related definition and functions + * @maixpy maix.image + */ +namespace maix::image +{ + + /** + * map point position or rectangle position from one image size to another image size(resize) + * @param int w_in original image width + * @param int h_in original image height + * @param int w_out target image width + * @param int h_out target image height + * @param fit resize method, see maix.image.Fit + * @param x original point x, or rectagle left-top point's x + * @param y original point y, or rectagle left-top point's y + * @param w original rectagle width, can be -1 if not use this arg, default -1. + * @param h original rectagle height, can be -1 if not use this arg, default -1. + * @return list type, [x, y] if map point, [x, y, w, h] if resize rectangle. + * @maixpy maix.image.resize_map_pos + */ + std::vector resize_map_pos(int w_in, int h_in, int w_out, int h_out, image::Fit fit, int x, int y, int w = -1, int h = -1); + + /** + * reverse resize_map_pos method, when we call image.resize method resiz image 'a' to image 'b', we want to known the original position on 'a' whith a knew point on 'b' + * @param int w_in original image width + * @param int h_in original image height + * @param int w_out image width after resized + * @param int h_out image height after resized + * @param fit resize method, see maix.image.Fit + * @param x point on resized image x, or rectagle left-top point's x + * @param y original point y, or rectagle left-top point's y + * @param w original rectagle width, can be -1 if not use this arg, default -1. + * @param h original rectagle height, can be -1 if not use this arg, default -1. + * @return list type, [x, y] if map point, [x, y, w, h] if resize rectangle. + * @maixpy maix.image.resize_map_pos_reverse + */ + std::vector resize_map_pos_reverse(int w_in, int h_in, int w_out, int h_out, image::Fit fit, int x, int y, int w = -1, int h = -1); + + /** + * Image class + * @maixpy maix.image.Image + */ + class Image + { + public: + /** + * Image constructor + * + * @param width image width, should > 0 + * @param height image height, should > 0 + * @param format image format @see image::Format + * @maixpy maix.image.Image.__init__ + * @maixcdk maix.image.Image.Image + */ + Image(int width, int height, image::Format format = image::Format::FMT_RGB888); + // Image(int width, int height, image::Format format = image::Format::FMT_RGB888, Bytes *data = nullptr, bool copy = true); + + /** + * Image constructor + * + * @param width image width, should > 0 + * @param height image height, should > 0 + * @param format image format @see image::Format + * @param data image data, if data is nullptr, will malloc memory for image data + * If the image is in jpeg format, data must be filled in. + * @param data_size image data size, only for compressed format like jpeg png, data_size must be filled in, or should be -1, default is -1. + * @param copy if true and data is not nullptr, will copy data to new buffer, else will use data directly. default is true to avoid memory leak. + * @maixcdk maix.image.Image.Image + */ + Image(int width, int height, image::Format format, uint8_t *data, int data_size, bool copy); + + Image() { + _width = 0; + _height = 0; + _format = image::Format::FMT_INVALID; + _data = nullptr; + _data_size = 0; + _is_malloc = false; + } + ~Image(); + + void operator=(const image::Image &img); + + //************************** get and set basic info **************************// + + /** + * Get image's format @see image.Format + * @maixpy maix.image.Image.format + */ + image::Format format() { return this->_format; } + + /** + * Get image's size, [width, height] + * @maixpy maix.image.Image.size + */ + image::Size size() { return Size(_width, _height); } + + std::vector shape() { return std::vector{_height, _width, (int)fmt_size[_format]}; } + + /** + * Get image's data size + * @maixpy maix.image.Image.data_size + */ + int data_size() { return _data_size; } + + /** + * Get image's width + * @maixpy maix.image.Image.width + */ + int width() { return _width; } + + /** + * Get image's height + * @maixpy maix.image.Image.height + */ + int height() { return _height; } + + /** + * Get image's data pointer. + * In MaixPy is capsule object. + * @maixpy maix.image.Image.data + */ + void *data(){ return _data; } + + /** + * To string method + * @maixpy maix.image.Image.__str__ + */ + std::string __str__(); + + /** + * To string method + * @maixpy maix.image.Image.to_str + */ + std::string to_str() { return __str__(); } + + /** + * Get pixel of image + * @param x pixel's coordinate x. x must less than image's width + * @param y pixel's coordinate y. y must less than image's height + * @param rgbtuple switch return value method. rgbtuple decides whether to split the return or not. default is false. + * @return pixel value, + * According to image format and rgbtuple, return different value: + * format is FMT_RGB888, rgbtuple is true, return [R, G, B]; rgbtuple is false, return [RGB] + * foramt is FMT_BGR888, rgbtuple is true, return [B, G, R]; rgbtuple is false, return [BGR] + * format is FMT_GRAYSCALE, return [GRAY]; + * + * @maixpy maix.image.Image.get_pixel + */ + std::vector get_pixel(int x, int y, bool rgbtuple = false) { + std::vector pixels; + if (!(_format == image::Format::FMT_RGB888 || _format == image::Format::FMT_BGR888 || + _format == image::Format::FMT_RGB565 || _format == image::Format::FMT_BGR565 || + _format == image::Format::FMT_GRAYSCALE)) { + log::error("get_pixel not support format: %d\r\n", _format); + return pixels; + } + + if (x < 0 || x >= _width || y < 0 || y >= _height) { + log::error("get_pixel out of range: (%d, %d)\r\n", x, y); + return pixels; + } + + switch (_format) { + case image::Format::FMT_RGB888: // fall through + case image::Format::FMT_BGR888: + { + uint8_t v0 = ((uint8_t *)_data)[y * _width * 3 + x * 3 + 0]; + uint8_t v1 = ((uint8_t *)_data)[y * _width * 3 + x * 3 + 1]; + uint8_t v2 = ((uint8_t *)_data)[y * _width * 3 + x * 3 + 2]; + if (!rgbtuple) { + int value = v0 << 16 | v1 << 8 | v2; + pixels.push_back(value); + } else { + pixels.push_back(v0); + pixels.push_back(v1); + pixels.push_back(v2); + } + break; + } + case image::Format::FMT_GRAYSCALE: + pixels.push_back(((uint8_t *)_data)[y * _width + x]); + break; + case image::Format::FMT_BGR565: // fall through + case image::Format::FMT_RGB565: + { + if (!rgbtuple) { + int value = ((uint16_t *)_data)[y * _width + x] & 0xffff; + pixels.push_back(value); + } else { + int value = ((uint16_t *)_data)[y * _width + x] & 0xffff; + int v0 = (value >> 11) & 0x1F; + int v1 = (value >> 5) & 0x3F; + int v2 = value & 0x1F; + pixels.push_back(v0); + pixels.push_back(v1); + pixels.push_back(v2); + } + break; + } + default: + log::error("get_pixel not support format: %d\r\n", _format); + break; + } + + return pixels; + } + + /** + * Set pixel of image + * @param x pixel's coordinate x. x must less than image's width + * @param y pixel's coordinate y. y must less than image's height + * @param pixel pixel value, according to image format and size of pixel, has different operation: + * format is FMT_RGB888, pixel size must be 1 or 3, if size is 1, will split pixel[0] to [R, G, B]; if size is 3, will use pixel directly + * format is FMT_BGR888, pixel size must be 1 or 3, if size is 1, will split pixel[0] to [B, G, R]; if size is 3, will use pixel directly + * format is FMT_GRAYSCALE, pixel size must be 1, will use pixel directly + * @return error code, Err::ERR_NONE is ok, other is error + * @maixpy maix.image.Image.set_pixel + */ + err::Err set_pixel(int x, int y, std::vector pixel) { + if (!(_format == image::Format::FMT_RGB888 || _format == image::Format::FMT_BGR888 || + _format == image::Format::FMT_RGB565 || _format == image::Format::FMT_BGR565 || + _format == image::Format::FMT_GRAYSCALE)) { + log::error("get_pixel not support format: %d\r\n", _format); + return err::Err::ERR_RUNTIME; + } + + if (x < 0 || x >= _width || y < 0 || y >= _height) { + log::error("get_pixel out of range: (%d, %d)\r\n", x, y); + return err::Err::ERR_RUNTIME; + } + + switch (_format) { + case image::Format::FMT_RGB888: // fall through + case image::Format::FMT_BGR888: + if (pixel.size() == 1) { + uint8_t v0 = pixel[0]; + ((uint8_t *)_data)[y * _width * 3 + x * 3 + 0] = (v0 >> 16) & 0xFF; + ((uint8_t *)_data)[y * _width * 3 + x * 3 + 1] = (v0 >> 8) & 0xFF; + ((uint8_t *)_data)[y * _width * 3 + x * 3 + 2] = v0 & 0xFF; + } else if (pixel.size() == 3) { + ((uint8_t *)_data)[y * _width * 3 + x * 3 + 0] = pixel[0]; + ((uint8_t *)_data)[y * _width * 3 + x * 3 + 1] = pixel[1]; + ((uint8_t *)_data)[y * _width * 3 + x * 3 + 2] = pixel[2]; + } else { + log::error("set_pixel pixel size must be 1 or 3, but %d\r\n", pixel.size()); + return err::Err::ERR_RUNTIME; + } + break; + case image::Format::FMT_GRAYSCALE: + if (pixel.size() == 1) { + ((uint8_t *)_data)[y * _width + x] = pixel[0]; + } else { + log::error("set_pixel pixel size must be 1, but %d\r\n", pixel.size()); + return err::Err::ERR_RUNTIME; + } + break; + case image::Format::FMT_BGR565: // fall through + case image::Format::FMT_RGB565: + { + if (pixel.size() == 1) { + ((uint16_t *)_data)[y * _width + x] = pixel[0]; + } else if (pixel.size() == 3) { + int num = ((pixel[0] & 0x1F) << 11) | ((pixel[1] & 0x3F) << 5) | (pixel[2] & 0x1F); + ((uint16_t *)_data)[y * _width + x] = num; + } else { + log::error("set_pixel pixel size must be 1 or 3, but %d\r\n", pixel.size()); + return err::Err::ERR_RUNTIME; + } + break; + } + + default: + log::error("get_pixel not support format: %d\r\n", _format); + break; + } + + return err::Err::ERR_NONE; + } + + //************************** convert format **************************// + // more maixpy convert func in MaixPy project's convert_image.hpp + + /** + * Convert Image object to tensor::Tensor object + * @param chw if true, the shape of tensor is [C, H, W], else [H, W, C] + * @param copy if true, will alloc memory for tensor data, else will use the memory of Image object + * @return tensor::Tensor object pointer, an allocated tensor object + * @maixpy maix.image.Image.to_tensor + */ + tensor::Tensor *to_tensor(bool chw = false, bool copy = true); + + /** + * Get image's data and convert to array bytes + * @param copy if true, will alloc memory and copy data to new buffer, + * else will use the memory of Image object, delete bytes object will not affect Image object, + * but delete Image object will make bytes object invalid, it may cause program crash !!!! + * So use this param carefully. + * @return image's data bytes, need be delete by caller in C++. + * @maixpy maix.image.Image.to_bytes + */ + Bytes *to_bytes(bool copy = true); + + /** + * Convert image to specific format + * @param format format want to convert to, @see image::Format, only support RGB888, BGR888, RGBA8888, BGRA8888, GRAYSCALE, JPEG. + * @return new image object. Need be delete by caller in C++. + * @throw err.Exception, if two images' format not support, **or already the format**, will raise exception + * @maixpy maix.image.Image.to_format + */ + image::Image *to_format(const image::Format &format); + + + /** + * Convert image to specific format + * @param format format want to convert to, @see image::Format, only support RGB888, BGR888, RGBA8888, BGRA8888, GRAYSCALE, JPEG. + * @param buff user's buffer, if buff is nullptr, will malloc memory for new image data, else will use buff directly + * @return new image object. Need be delete by caller in C++. + * @throw err.Exception, if two images' format not support, **or already the format**, will raise exception + * @maixcdk maix.image.Image.to_format + */ + image::Image *to_format(const image::Format &format, void *buff, size_t buff_size); + + /** + * Convert image to jpeg + * @param quality the quality of jpg, default is 95. range is (50, 100]. + * @return new image object. Need be delete by caller in C++. + * @throw err.Exception, if two images' format not support, **or already the format**, will raise exception + * @maixpy maix.image.Image.to_jpeg + */ + image::Image *to_jpeg(int quality = 95); + + //************************** draw **************************// + + /** + * Draw image on this image + * @param x left top corner of image point's coordinate x + * @param y left top corner of image point's coordinate y + * @param img image object to draw, the caller's channel must <= the args' channel, + * e.g. caller is RGB888, args is RGBA8888, will throw exception, but caller is RGBA8888, args is RGB888 or RGBA8888 is ok + * @return this image object self + * @maixpy maix.image.Image.draw_image + */ + image::Image *draw_image(int x, int y, image::Image &img); + + /** + * Fill rectangle color to image + * @param x left top corner of rectangle point's coordinate x + * @param y left top corner of rectangle point's coordinate y + * @param w rectangle width + * @param h rectangle height + * @param color rectangle color + * @param thickness rectangle thickness(line width), by default(value is 1), -1 means fill rectangle + * @return this image object self + * @maixpy maix.image.Image.draw_rect + */ + image::Image *draw_rect(int x, int y, int w, int h, const image::Color &color, int thickness = 1); + + /** + * Draw line on image + * @param x1 start point's coordinate x + * @param y1 start point's coordinate y + * @param x2 end point's coordinate x + * @param y2 end point's coordinate y + * @param color line color @see image::Color + * @param thickness line thickness(line width), by default(value is 1) + * @return this image object self + * @maixpy maix.image.Image.draw_line + */ + image::Image *draw_line(int x1, int y1, int x2, int y2, const image::Color &color, int thickness = 1); + + /** + * Draw circle on image + * @param x circle center point's coordinate x + * @param y circle center point's coordinate y + * @param radius circle radius + * @param color circle color @see image::Color + * @param thickness circle thickness(line width), by default(value is 1), -1 means fill circle + * @return this image object self + * @maixpy maix.image.Image.draw_circle + */ + image::Image *draw_circle(int x, int y, int radius, const image::Color &color, int thickness = 1); + + /** + * Draw ellipse on image + * @param x ellipse center point's coordinate x + * @param y ellipse center point's coordinate y + * @param a ellipse major axis length + * @param b ellipse minor axis length + * @param angle ellipse rotation angle + * @param start_angle ellipse start angle + * @param end_angle ellipse end angle + * @param color ellipse color @see image::Color + * @param thickness ellipse thickness(line width), by default(value is 1), -1 means fill ellipse + * @return this image object self + * @maixpy maix.image.Image.draw_ellipse + */ + image::Image *draw_ellipse(int x, int y, int a, int b, float angle, float start_angle, float end_angle, const image::Color &color, int thickness = 1); + + /** + * Draw text on image + * @param x text left top point's coordinate x + * @param y text left top point's coordinate y + * @param string text content + * @param color text color @see image::Color, default is white + * @param scale font scale, by default(value is 1) + * @param thickness text thickness(line width), if negative, the glyph is filled, by default(value is -1) + * @param wrap if true, will auto wrap text to next line if text width > image width, by default(value is true) + * @return this image object self + * @maixpy maix.image.Image.draw_string + */ + image::Image *draw_string(int x, int y, const std::string &textstring, const image::Color &color = image::COLOR_WHITE, float scale = 1, int thickness = -1, + bool wrap = true, int wrap_space = 4, const std::string &font = ""); + + /** + * Draw cross on image + * @param x cross center point's coordinate x + * @param y cross center point's coordinate y + * @param color cross color @see image::Color + * @param size how long the lines of the cross extend, by default(value is 5). So the line length is `2 * size + thickness` + * @param thickness cross thickness(line width), by default(value is 1) + * @maixpy maix.image.Image.draw_cross + */ + image::Image *draw_cross(int x, int y, const image::Color &color, int size = 5, int thickness = 1); + + /** + * Draw arrow on image + * @param x0 start coordinate of the arrow x0 + * @param y0 start coordinate of the arrow y0 + * @param x1 end coordinate of the arrow x1 + * @param y1 end coordinate of the arrow y1 + * @param color cross color @see image::Color + * @param thickness cross thickness(line width), by default(value is 1) + * @return this image object self + * @maixpy maix.image.Image.draw_arrow + */ + image::Image *draw_arrow(int x0, int y0, int x1, int y1, const image::Color &color, int thickness = 1); + + /** + * Draw edges on image + * @param corners edges, [[x0, y0], [x1, y1], [x2, y2], [x3, y3]] + * @param color edges color @see image::Color + * @param size the circle of radius size. TODO: support in the feature + * @param thickness edges thickness(line width), by default(value is 1) + * @param fill if true, will fill edges, by default(value is false) + * @return this image object self + * @maixpy maix.image.Image.draw_edges + */ + image::Image *draw_edges(std::vector> corners, const image::Color &color, int size = 0, int thickness = 1, bool fill = false); + + /** + * Draw keypoints on image + * @param keypoints keypoints, [x1, y1, x2, y2...] or [x, y, rotation_andle_in_degrees, x2, y2, rotation_andle_in_degrees2](TODO: rotation_andle_in_degrees support in the feature) + * @param color keypoints color @see image::Color + * @param size size of keypoints + * @param thickness keypoints thickness(line width), by default(value is -1 means fill circle) + * @return this image object self + * @maixpy maix.image.Image.draw_keypoints + */ + image::Image *draw_keypoints(std::vector keypoints, const image::Color &color, int size = 10, int thickness = -1); + + //************************** image operations **************************// + + /** + * Resize image, will create a new resized image object + * @param width new width, if value is -1, will use height to calculate aspect ratio + * @param height new height, if value is -1, will use width to calculate aspect ratio + * @param object_fit fill, contain, cover, by default is fill + * @param method resize method, by default is bilinear + * @return Always return a new resized image object even size not change, So in C++ you should take care of the return value to avoid memory leak. + * And it's better to judge whether the size has changed before calling this function to make the program more efficient. + * e.g. + * if img->width() != width || img->height() != height: + * img = img->resize(width, height); + * @maixpy maix.image.Image.resize + */ + image::Image *resize(int width, int height, image::Fit object_fit = image::Fit::FIT_FILL, image::ResizeMethod method = image::ResizeMethod::NEAREST); + + /** + * Affine transform image, will create a new transformed image object + * @param src_points three source points, [x1, y1, x2, y2, x3, y3] + * @param dst_points three destination points, [x1, y1, x2, y2, x3, y3] + * @param width new width, if value is -1, will use height to calculate aspect ratio + * @param height new height, if value is -1, will use width to calculate aspect ratio + * @param method resize method, by default is bilinear + * @return new transformed image object + * @maixpy maix.image.Image.affine + */ + image::Image *affine(std::vector src_points, std::vector dst_points, int width = -1, int height = -1, image::ResizeMethod method = image::ResizeMethod::BILINEAR); + + /** + * Copy image, will create a new copied image object + * @return new copied image object + * @maixpy maix.image.Image.copy + */ + image::Image *copy(); + + /** + * Crop image, will create a new cropped image object + * @param x left top corner of crop rectangle point's coordinate x + * @param y left top corner of crop rectangle point's coordinate y + * @param w crop rectangle width + * @param h crop rectangle height + * @return new cropped image object + * @maixpy maix.image.Image.crop + */ + image::Image *crop(int x, int y, int w, int h); + + /** + * Rotate image, will create a new rotated image object + * @param angle anti-clock wise rotate angle, if angle is 90 or 270, and width or height is -1, will swap width and height, or will throw exception + * @param width new width, if value is -1, will use height to calculate aspect ratio + * @param height new height, if value is -1, will use width to calculate aspect ratio + * @param method resize method, by default is bilinear + * @return new rotated image object + * @maixpy maix.image.Image.rotate + */ + image::Image *rotate(float angle, int width = -1, int height = -1, image::ResizeMethod method = image::ResizeMethod::BILINEAR); + + /** + * @brief Finds the mean of x_div * y_div squares in the image and returns the modified image composed of the mean of each square. + * @param x_div The width of the squares. + * @param y_div The height of the squares. + * @param copy Select whether to return a new image or modify the original image. default is false. + * If true, returns a new image composed of the mean of each square; If false, returns the modified image composed of the mean of each square. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.mean_pool + */ + image::Image *mean_pool(int x_div, int y_div, bool copy = false); + + /** + * @brief Finds the midpoint of x_div * y_div squares in the image and returns the modified image composed of the mean of each square. + * @param x_div The width of the squares. + * @param y_div The height of the squares. + * @param bias The bias of the midpoint. default is 0.5. + * midpoint value is equal to (max * bias + min * (1 - bias)) + * @param copy Select whether to return a new image or modify the original image. default is false. + * If true, returns a new image composed of the midpoint of each square; If false, returns the modified image composed of the midpoint of each square. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.midpoint_pool + */ + image::Image *midpoint_pool(int x_div, int y_div, double bias = 0.5, bool copy = false); + + /** + * @brief JPEG compresses the image in place, the same as to_jpeg functioin, it's recommend to use to_jpeg instead. + * @param quality The quality of the compressed image. default is 95. + * @return Returns the compressed JPEG image + * @maixpy maix.image.Image.compress + */ + image::Image *compress(int quality = 95); + + /** + * @brief Sets all pixels in the image to zero + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.clear + */ + image::Image *clear(image::Image *mask = nullptr); + + /** + * @brief Zeros a rectangular part of the image. If no arguments are supplied this method zeros the center of the image. + * @param x The x coordinate of the top left corner of the rectangle. + * @param y The y coordinate of the top left corner of the rectangle. + * @param w The width of the rectangle. + * @param h The height of the rectangle. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.mask_rectange + */ + image::Image *mask_rectange(int x = -1, int y = -1, int w = -1, int h = -1); + + /** + * @brief Zeros a circular part of the image. If no arguments are supplied this method zeros the center of the image. + * @param x The x coordinate of the center of the circle. + * @param y The y coordinate of the center of the circle. + * @param radius The radius of the circle. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.mask_circle + */ + image::Image *mask_circle(int x = -1, int y = -1, int radius = -1); + + /** + * @brief Zeros a ellipse part of the image. If no arguments are supplied this method zeros the center of the image. + * @param x The x coordinate of the center of the ellipse. + * @param y The y coordinate of the center of the ellipse. + * @param radius_x The radius of the ellipse in the x direction. + * @param radius_y The radius of the ellipse in the y direction. + * @param rotation_angle_in_degrees The rotation angle of the ellipse in degrees. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.mask_ellipse + */ + image::Image *mask_ellipse(int x = -1, int y = -1, int radius_x = -1, int radius_y = -1, float rotation_angle_in_degrees = 0); + + /** + * @brief Sets all pixels in the image to black or white depending on if the pixel is inside of a threshold in the threshold list thresholds or not. + * @param thresholds You can define multiple thresholds. + * For GRAYSCALE format, you can use {{Lmin, Lmax}, ...} to define one or more thresholds. + * For RGB888 format, you can use {{Lmin, Lmax, Amin, Amax, Bmin, Bmax}, ...} to define one or more thresholds. + * Where the upper case L,A,B represent the L,A,B channels of the LAB image format, and min, max represent the minimum and maximum values of the corresponding channels. + * @param invert If true, the thresholds will be inverted before the operation. default is false. + * @param zero If zero is true, the image will be set the pixels within the threshold to 0, other pixels remain unchanged. If zero is false, the image will be set to black or white. default is false. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @param to_bitmap If true, the image will be converted to a bitmap image before thresholding. default is false. TODO: support in the feature + * @param copy Select whether to return a new image or modify the original image. default is false. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.binary + */ + image::Image *binary(std::vector> thresholds = std::vector>(), bool invert = false, bool zero = false, image::Image *mask = nullptr, bool to_bitmap = false, bool copy = false); + + /** + * @brief Inverts the image in place. + * @return Returns the image after the operation is completed + * @maixpy maix.image.Image.invert + */ + image::Image *invert(); + + /** + * @brief Performs a bitwise and operation between the image and the other image. + * @param other The other image should be an image and should be the same size as the image being operated on. TODO: support path? + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.b_and + */ + image::Image *b_and(image::Image *other, image::Image *mask = nullptr); + + /** + * @brief Performs a bitwise nand operation between the image and the other image. + * @param other The other image should be an image and should be the same size as the image being operated on. TODO: support path? + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.b_nand + */ + image::Image *b_nand(image::Image *other, image::Image *mask = nullptr); + + /** + * @brief Performs a bitwise or operation between the image and the other image. + * @param other The other image should be an image and should be the same size as the image being operated on. TODO: support path? + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.b_or + */ + image::Image *b_or(image::Image *other, image::Image *mask = nullptr); + + /** + * @brief Performs a bitwise nor operation between the image and the other image. + * @param other The other image should be an image and should be the same size as the image being operated on. TODO: support path? + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.b_nor + */ + image::Image *b_nor(image::Image *other, image::Image *mask = nullptr); + + /** + * @brief Performs a bitwise xor operation between the image and the other image. + * @param other The other image should be an image and should be the same size as the image being operated on. TODO: support path? + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.b_xor + */ + image::Image *b_xor(image::Image *other, image::Image *mask = nullptr); + + /** + * @brief Performs a bitwise xnor operation between the image and the other image. + * @param other The other image should be an image and should be the same size as the image being operated on. TODO: support path? + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.b_xnor + */ + image::Image *b_xnor(image::Image *other, image::Image *mask = nullptr); + + /** + * @brief Performs an auto white balance operation on the image. TODO: support in the feature + * @param max if True uses the white-patch algorithm instead. default is false. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.awb + */ + image::Image *awb(bool max = false); + + /** + * @brief Multiples the passed (3x3) or (4x3) floating-point color-correction-matrix with the image. + * note: Grayscale format is not support. + * @param matrix The color correction matrix to use. 3x3 or 4x3 matrix. + * Weights may either be positive or negative, and the sum of each column in the 3x3 matrix should generally be 1. + * example: + * { + * 1, 0, 0, + * 0, 1, 0, + * 0, 0, 1, + * } + * + * Where the last row of the 4x3 matrix is an offset per color channel. If you add an offset you may wish to make the + * weights sum to less than 1 to account for the offset. + * example: + * { + * 1, 0, 0, + * 0, 1, 0, + * 0, 0, 1, + * 0, 0, 0, + * } + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.ccm + */ + image::Image *ccm(std::vector &matrix); + + /** + * @brief Quickly changes the image gamma, contrast, and brightness. Create a array whose size is usually 255, + * and use the parameters gamma, contrast, and brightness to calculate the value of the array, and then map the + * image pixel value through the value of the array. + * The calculation method for array is: array[array_idx] = (powf((array_idx / 255.0), (1 / gamma)) * contrast + brightness) * scale, + * `powf` is a function used to calculate floating point power. + * `array` is the array used for mapping. + * `array_idx` is the index of the array, the maximum value is determined according to the image format, usually 255. + * `scale` is a constant, the value is determined by the image format, usually 255. + * Mapping method: + * Assume that a pixel value in the image is 128, then map the pixel value to the value of array[128] + * Users can adjust the value of the array through the gamma, contrast, and brightness parameters. + * @param gamma The contrast gamma greater than 1.0 makes the image darker in a non-linear manner while less than 1.0 makes the image brighter. default is 1.0. + * @param contrast The contrast value greater than 1.0 makes the image brighter in a linear manner while less than 1.0 makes the image darker. default is 1.0. + * @param brightness The brightness value greater than 0.0 makes the image brighter in a constant manner while less than 0.0 makes the image darker. default is 0.0. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.gamma + */ + image::Image *gamma(double gamma = 1.0, double contrast = 1.0, double brightness = 0.0); + + /** + * @brief Alias for Image.gamma. + * @param gamma The contrast gamma greater than 1.0 makes the image darker in a non-linear manner while less than 1.0 makes the image brighter. default is 1.0. + * @param contrast The contrast value greater than 1.0 makes the image brighter in a linear manner while less than 1.0 makes the image darker. default is 1.0. + * @param brightness The brightness value greater than 0.0 makes the image brighter in a constant manner while less than 0.0 makes the image darker. default is 0.0. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.gamma_corr + */ + image::Image *gamma_corr(double gamma, double contrast = 1.0, double brightness = 0.0); + + /** + * @brief Flips (numerically inverts) all pixels values in an image + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.negate + */ + image::Image *negate(); + + /** + * @brief Replaces all pixels in the image with the corresponding pixels in the other image. + * @param other The other image should be an image and should be the same size as the image being operated on. + * @param hmirror If true, the image will be horizontally mirrored before the operation. default is false. + * @param vflip If true, the image will be vertically flipped before the operation. default is false. + * @param transpose If true, the image can be used to rotate 90 degrees or 270 degrees. + * hmirror = false, vflip = false, transpose = false, the image will not be rotated. + * hmirror = false, vflip = true, transpose = true, the image will be rotated 90 degrees. + * hmirror = true, vflip = true, transpose = false, the image will be rotated 180 degrees. + * hmirror = true, vflip = false, transpose = true, the image will be rotated 270 degrees. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.replace + */ + image::Image *replace(image::Image *other = nullptr, bool hmirror = false, bool vflip = false, bool transpose = false, image::Image *mask = nullptr); + + /** + * @brief Alias for Image::replace. + * @param other The other image should be an image and should be the same size as the image being operated on. + * @param hmirror If true, the image will be horizontally mirrored before the operation. default is false. + * @param vflip If true, the image will be vertically flipped before the operation. default is false. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.set + */ + image::Image *set(image::Image *other, bool hmirror = false, bool vflip = false, bool transpose = false, image::Image *mask = nullptr); + + /** + * @brief Adds the other image to the image. + * @param other The other image should be an image and should be the same size as the image being operated on. TODO: support path? + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.add + */ + image::Image *add(image::Image *other, image::Image *mask = nullptr); + + /** + * @brief Subtracts the other image from the image. + * @param other The other image should be an image and should be the same size as the image being operated on. TODO: support path? + * @param reverse If true, the image will be reversed before the operation. default is false. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.sub + */ + image::Image *sub(image::Image *other, bool reverse = false, image::Image *mask = nullptr); + + /** + * @brief Multiplies the image by the other image. + * Note: This method is meant for image blending and cannot multiply the pixels in the image by a scalar like 2. + * @param other The other image should be an image and should be the same size as the image being operated on. TODO: support path? + * @param invert If true, the image will be change the multiplication operation from a*b to 1/((1/a)*(1/b)). + * In particular, this lightens the image instead of darkening it (e.g. multiply versus burn operations). default is false. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.mul + */ + image::Image *mul(image::Image *other, bool invert = false, image::Image *mask = nullptr); + + /** + * @brief Divides the image by the other image. + * This method is meant for image blending and cannot divide the pixels in the image by a scalar like 2. + * @param other The other image should be an image and should be the same size as the image being operated on. TODO: support path? + * @param invert If true, the image will be change the division direction from a/b to b/a. default is false. + * @param mod If true, the image will be change the division operation to the modulus operation. default is false. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.div + */ + image::Image *div(image::Image *other, bool invert = false, bool mod = false, image::Image *mask = nullptr); + + /** + * @brief Caculate the minimum of each pixel in the image and the other image. + * @param other The other image should be an image and should be the same size as the image being operated on. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.min + */ + image::Image *min(image::Image *other, image::Image *mask = nullptr); + + /** + * @brief Caculate the maximum of each pixel in the image and the other image. + * @param other The other image should be an image and should be the same size as the image being operated on. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.max + */ + image::Image *max(image::Image *other, image::Image *mask = nullptr); + + /** + * @brief Caculate the absolute value of the difference between each pixel in the image and the other image. + * @param other The other image should be an image and should be the same size as the image being operated on. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.difference + */ + image::Image *difference(image::Image *other, image::Image *mask = nullptr); + + /** + * @brief Blends the image with the other image. + * res = alpha * this_img / 256 + (256 - alpha) * other_img / 256 + * @param other The other image should be an image and should be the same size as the image being operated on. + * @param alpha The alpha value of the blend, the value range is [0, 256],default is 128. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.blend + */ + image::Image *blend(image::Image *other, int alpha = 128, image::Image *mask = nullptr); + + /** + * @brief Runs the histogram equalization algorithm on the image. + * @param adaptive If true, an adaptive histogram equalization method will be run on the image instead which as generally better results than non-adaptive histogram qualization but a longer run time. default is false. + * @param clip_limit Provides a way to limit the contrast of the adaptive histogram qualization. Use a small value for this, like 10, to produce good histogram equalized contrast limited images. default is -1. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.histeq + */ + image::Image *histeq(bool adaptive = false, int clip_limit = -1, image::Image *mask = nullptr); + + /** + * @brief Standard mean blurring filter using a box filter. + * The parameters offset and invert are valid when threshold is True. + * @param size Kernel size. The actual kernel size is ((size * 2) + 1) * ((size * 2) + 1). Use 1(3x3 kernel), 2(5x5 kernel). + * @param threshold If true, which will enable adaptive thresholding of the image which sets pixels to white or black based on a pixel’s brightness in relation to the brightness of the kernel of pixels around them. + * default is false. + * @param offset The larger the offset value, the lower brightness pixels on the original image will be set to white. default is 0. + * @param invert If true, the image will be inverted before the operation. default is false. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.mean + */ + image::Image *mean(int size, bool threshold = false, int offset = 0, bool invert = false, image::Image *mask = nullptr); + + /** + * @brief Runs the median filter on the image. The median filter is the best filter for smoothing surfaces while preserving edges but it is very slow. + * @param size Kernel size. The actual kernel size is ((size * 2) + 1) * ((size * 2) + 1). Use 1(3x3 kernel), 2(5x5 kernel). + * @param percentile This parameter controls the percentile of the value used in the kernel. You can set this to 0 for a min filter, 0.25 for a lower quartile filter, 0.75 for an upper quartile filter, and 1.0 for a max filter. default is 0.5. + * @param threshold If true, which will enable adaptive thresholding of the image which sets pixels to white or black based on a pixel’s brightness in relation to the brightness of the kernel of pixels around them. + * default is false. + * @param offset The larger the offset value, the lower brightness pixels on the original image will be set to white. default is 0. + * @param invert If true, the image will be inverted before the operation. default is false. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.median + */ + image::Image *median(int size, double percentile = 0.5, bool threshold = false, int offset = 0, bool invert = false, image::Image *mask = nullptr); + + /** + * @brief Runs the mode filter on the image by replacing each pixel with the mode of their neighbors. + * @param size Kernel size. The actual kernel size is ((size * 2) + 1) * ((size * 2) + 1). Use 1(3x3 kernel), 2(5x5 kernel). + * @param threshold If true, which will enable adaptive thresholding of the image which sets pixels to white or black based on a pixel’s brightness in relation to the brightness of the kernel of pixels around them. + * default is false. + * @param offset The larger the offset value, the lower brightness pixels on the original image will be set to white. default is 0. + * @param invert If true, the image will be inverted before the operation. default is false. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.mode + */ + image::Image *mode(int size, bool threshold = false, int offset = 0, bool invert = false, image::Image *mask = nullptr); + + /** + * @brief Runs the midpoint filter on the image.This filter finds the midpoint (max * bias + min * (1 - bias)) of each pixel neighborhood in the image. + * @param size Kernel size. The actual kernel size is ((size * 2) + 1) * ((size * 2) + 1). Use 1(3x3 kernel), 2(5x5 kernel). + * @param bias The bias of the midpoint. default is 0.5. + * @param threshold If true, which will enable adaptive thresholding of the image which sets pixels to white or black based on a pixel’s brightness in relation to the brightness of the kernel of pixels around them. + * default is false. + * @param offset The larger the offset value, the lower brightness pixels on the original image will be set to white. default is 0. + * @param invert If true, the image will be inverted before the operation. default is false. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.midpoint + */ + image::Image *midpoint(int size, double bias = 0.5, bool threshold = false, int offset = 0, bool invert = false, image::Image *mask = nullptr); + + /** + * @brief Convolves the image by a filter kernel. This allows you to do general purpose convolutions on an image. + * @param size Kernel size. The actual kernel size is ((size * 2) + 1) * ((size * 2) + 1). Use 1(3x3 kernel), 2(5x5 kernel). + * @param kernel The kernel used for convolution. The kernel should be a list of lists of numbers. The kernel should be the same size as the actual kernel size. + * @param mul This parameter is used to multiply the convolved pixel results. default is auto. + * @param add This parameter is the value to be added to each convolution pixel result. default is 0.0. + * @param threshold If true, which will enable adaptive thresholding of the image which sets pixels to white or black based on a pixel’s brightness in relation to the brightness of the kernel of pixels around them. + * default is false. + * @param offset The larger the offset value, the lower brightness pixels on the original image will be set to white. default is 0. + * @param invert If true, the image will be inverted before the operation. default is false. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.morph + */ + image::Image *morph(int size, std::vector kernel, float mul = -1, float add = 0.0, bool threshold = false, int offset = 0, bool invert = false, image::Image *mask = nullptr); + + /** + * @brief Convolves the image by a smoothing guassian kernel. + * @param size Kernel size. The actual kernel size is ((size * 2) + 1) * ((size * 2) + 1). Use 1(3x3 kernel), 2(5x5 kernel). + * @param unsharp If true, this method will perform an unsharp mask operation instead of gaussian filtering operation, this improves the clarity of image edges. default is false. + * @param mul This parameter is used to multiply the convolved pixel results. default is auto. + * @param add This parameter is the value to be added to each convolution pixel result. default is 0.0. + * @param threshold If true, which will enable adaptive thresholding of the image which sets pixels to white or black based on a pixel’s brightness in relation to the brightness of the kernel of pixels around them. + * default is false. + * @param offset The larger the offset value, the lower brightness pixels on the original image will be set to white. default is 0. + * @param invert If true, the image will be inverted before the operation. default is false. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.gaussian + */ + image::Image *gaussian(int size, bool unsharp = false, float mul = -1, float add = 0.0, bool threshold = false, int offset = 0, bool invert = false, image::Image *mask = nullptr); + + /** + * @brief Convolves the image by a edge detecting laplacian kernel. + * @param size Kernel size. The actual kernel size is ((size * 2) + 1) * ((size * 2) + 1). Use 1(3x3 kernel), 2(5x5 kernel). + * @param sharpen If True, this method will sharpen the image instead of an unthresholded edge detection image. Then increase the kernel size to improve image clarity. default is false. + * @param mul This parameter is used to multiply the convolved pixel results. default is auto. + * @param add This parameter is the value to be added to each convolution pixel result. default is 0.0. + * @param threshold If true, which will enable adaptive thresholding of the image which sets pixels to white or black based on a pixel’s brightness in relation to the brightness of the kernel of pixels around them. + * default is false. + * @param offset The larger the offset value, the lower brightness pixels on the original image will be set to white. default is 0. + * @param invert If true, the image will be inverted before the operation. default is false. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.laplacian + */ + image::Image *laplacian(int size, bool sharpen = false, float mul = -1, float add = 0.0, bool threshold = false, int offset = 0, bool invert = false, image::Image *mask = nullptr); + + /** + * @brief Convolves the image by a bilateral filter. + * @param size Kernel size. The actual kernel size is ((size * 2) + 1) * ((size * 2) + 1). Use 1(3x3 kernel), 2(5x5 kernel). + * @param color_sigma Controls how closely colors are matched using the bilateral filter. default is 0.1. + * @param space_sigma Controls how closely pixels space-wise are blurred with each other. default is 1. + * @param threshold If true, which will enable adaptive thresholding of the image which sets pixels to white or black based on a pixel’s brightness in relation to the brightness of the kernel of pixels around them. + * default is false. + * @param offset The larger the offset value, the lower brightness pixels on the original image will be set to white. default is 0. + * @param invert If true, the image will be inverted before the operation. default is false. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.bilateral + */ + image::Image *bilateral(int size, double color_sigma = 0.1, double space_sigma = 1, bool threshold = false, int offset = 0, bool invert = false, image::Image *mask = nullptr); + + /** + * @brief Re-project’s and image from cartessian coordinates to linear polar coordinates. + * @param reverse If true, the image will be reverse polar transformed. default is false. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.linpolar + */ + image::Image *linpolar(bool reverse = false); + + /** + * @brief Re-project’s and image from cartessian coordinates to log polar coordinates. + * @param reverse If true, the image will be reverse polar transformed. default is false. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.logpolar + */ + image::Image *logpolar(bool reverse = false); + + /** + * @brief Performs a lens correction operation on the image. TODO: support in the feature + * @param strength The strength of the lens correction. default is 1.8. + * @param zoom The zoom of the lens correction. default is 1.0. + * @param x_corr The x correction of the lens correction. default is 0.0. + * @param y_corr The y correction of the lens correction. default is 0.0. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.lens_corr + */ + image::Image *lens_corr(double strength = 1.8, double zoom = 1.0, double x_corr = 0.0, double y_corr = 0.0); + + /** + * @brief Performs a rotation correction operation on the image. TODO: support in the feature + * @param x_rotation The x rotation of the rotation correction. default is 0.0. + * @param y_rotation The y rotation of the rotation correction. default is 0.0. + * @param z_rotation The z rotation of the rotation correction. default is 0.0. + * @param x_translation The x translation of the rotation correction. default is 0.0. + * @param y_translation The y translation of the rotation correction. default is 0.0. + * @param zoom The zoom of the rotation correction. default is 1.0. + * @param fov The fov of the rotation correction. default is 60.0. + * @param corners The corners of the rotation correction. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.rotation_corr + */ + image::Image *rotation_corr(double x_rotation = 0.0, double y_rotation = 0.0, double z_rotation = 0.0, double x_translation = 0.0, double y_translation = 0.0, double zoom = 1.0, double fov = 60.0, std::vector corners = std::vector()); + + /** + * @brief Gets the histogram of the image. + * @param thresholds You can define multiple thresholds. + * For GRAYSCALE format, you can use {{Lmin, Lmax}, ...} to define one or more thresholds. + * For RGB888 format, you can use {{Lmin, Lmax, Amin, Amax, Bmin, Bmax}, ...} to define one or more thresholds. + * Where the upper case L,A,B represent the L,A,B channels of the LAB image format, and min, max represent the minimum and maximum values of the corresponding channels. + * @param invert If true, the thresholds will be inverted before the operation. default is false. + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @param bins The number of bins to use for the histogram. + * In GRAYSCALE format, setting range is [2, 256], default is 100. + * In rgb888 format, setting range is [2, 100], default is 100. + * @param l_bins The number of bins to use for the l channel of the histogram. Only valid in RGB888 format. + * If an invalid value is set, bins will be used instead. The setting range is [2, 100], default is 100. + * @param a_bins The number of bins to use for the a channel of the histogram. + * Only valid in RGB888 format.The setting range is [2, 256], default is 256. + * @param b_bins The number of bins to use for the b channel of the histogram. + * Only valid in RGB888 format. The setting range is [2, 256], default is 256. + * @param difference difference may be set to an image object to cause this method to operate on the difference image between the current image and the difference image object. + * default is None. + * @return Returns the histogram of the image + * @maixpy maix.image.Image.get_histogram + */ + std::map> get_histogram(std::vector> thresholds = std::vector>(), bool invert = false, std::vector roi = std::vector(), int bins = -1, int l_bins = 100, int a_bins = 256, int b_bins = 256, image::Image *difference = nullptr); + + /** + * @brief Gets the statistics of the image. TODO: support in the feature + * @param thresholds You can define multiple thresholds. + * For GRAYSCALE format, you can use {{Lmin, Lmax}, ...} to define one or more thresholds. + * For RGB888 format, you can use {{Lmin, Lmax, Amin, Amax, Bmin, Bmax}, ...} to define one or more thresholds. + * Where the upper case L,A,B represent the L,A,B channels of the LAB image format, and min, max represent the minimum and maximum values of the corresponding channels. + * @param invert If true, the image will be inverted before the operation. default is false. + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @param bins The number of bins to use for the statistics. default is -1. + * @param l_bins The number of bins to use for the l channel of the statistics. default is -1. + * @param a_bins The number of bins to use for the a channel of the statistics. default is -1. + * @param b_bins The number of bins to use for the b channel of the statistics. default is -1. + * @param difference The difference image to use for the statistics. default is None. + * @return Returns the statistics of the image + * @maixpy maix.image.Image.get_statistics + */ + image::Statistics get_statistics(std::vector> thresholds = std::vector>(), bool invert = false, std::vector roi = std::vector(), int bins = -1, int l_bins = -1, int a_bins = -1, int b_bins = -1, image::Image *difference = nullptr); + + /** + * @brief Gets the regression of the image. + * @param thresholds You can define multiple thresholds. + * For GRAYSCALE format, you can use {{Lmin, Lmax}, ...} to define one or more thresholds. + * For RGB888 format, you can use {{Lmin, Lmax, Amin, Amax, Bmin, Bmax}, ...} to define one or more thresholds. + * Where the upper case L,A,B represent the L,A,B channels of the LAB image format, and min, max represent the minimum and maximum values of the corresponding channels. + * @param invert If true, the image will be inverted before the operation. default is false. + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @param x_stride The x stride to use for the regression. default is 2. + * @param y_stride The y stride to use for the regression. default is 1. + * @param area_threshold The area threshold to use for the regression. default is 10. + * @param pixels_threshold The pixels threshold to use for the regression. default is 10. + * @param robust If true, the regression will be robust. default is false. + * @return Returns the regression of the image + * @maixpy maix.image.Image.get_regression + */ + std::vector get_regression(std::vector> thresholds = std::vector>(), bool invert = false, std::vector roi = std::vector(), int x_stride = 2, int y_stride = 1, int area_threshold = 10, int pixels_threshold = 10, bool robust = false); + + //************************** image with filesystem **************************// + /** + * Save image to file + * @param path file path + * @param quality image quality, by default(value is 95), support jpeg and png format + * @return error code, err::ERR_NONE is ok, other is error + * @maixpy maix.image.Image.save + */ + err::Err save(const char *path, int quality = 95); + + //************************** application algorithms **************************// + /** + * @brief Flood fills a region of the image starting from location x, y. + * @param x The x coordinate of the seed point. + * @param y The y coordinate of the seed point. + * @param seed_threshold The seed_threshold value controls how different any pixel in the fill area may be from the original starting pixel. default is 0.05. + * @param floating_threshold The floating_threshold value controls how different any pixel in the fill area may be from any neighbor pixels. default is 0.05. + * @param color The color to fill the region with. default is white. + * @param invert If true, the image will be inverted before the operation. default is false. + * @param clear_background If true, the background will be cleared before the operation. default is false. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. FIXME: the mask image works abnormally + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.flood_fill + */ + image::Image *flood_fill(int x, int y, float seed_threshold = 0.05, float floating_threshold = 0.05, image::Color color = image::COLOR_WHITE, bool invert = false, bool clear_background = false, image::Image *mask = nullptr); + + /** + * @brief Erodes the image in place. + * @param size Kernel size. The actual kernel size is ((size * 2) + 1) * ((size * 2) + 1). Use 1(3x3 kernel), 2(5x5 kernel). + * @param threshold The number of pixels in the kernel that are not 0. If it is less than or equal to the threshold, set the center pixel to black. default is (kernel_size - 1). + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.erode + */ + image::Image *erode(int size, int threshold = -1, image::Image *mask = nullptr); + + /** + * @brief Dilates the image in place. + * @param size Kernel size. The actual kernel size is ((size * 2) + 1) * ((size * 2) + 1). Use 1(3x3 kernel), 2(5x5 kernel). + * @param threshold The number of pixels in the kernel that are not 0. If it is greater than or equal to the threshold, set the center pixel to white. default is 0. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.dilate + */ + image::Image *dilate(int size, int threshold = 0, image::Image *mask = nullptr); + + /** + * @brief Performs erosion and dilation on an image in order. + * @param size Kernel size. The actual kernel size is ((size * 2) + 1) * ((size * 2) + 1). Use 1(3x3 kernel), 2(5x5 kernel). + * @param threshold As the threshold for erosion and dilation, the actual threshold for erosion is (kernel_size - 1 - threshold), the actual threshold for dialation is threshold. default is 0. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.open + */ + image::Image *open(int size, int threshold = 0, image::Image *mask = nullptr); + + /** + * @brief Performs dilation and erosion on an image in order. + * @param size Kernel size. The actual kernel size is ((size * 2) + 1) * ((size * 2) + 1). Use 1(3x3 kernel), 2(5x5 kernel). + * @param threshold As the threshold for erosion and dilation, the actual threshold for erosion is (kernel_size - 1 - threshold), the actual threshold for dialation is threshold. default is 0. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.close + */ + image::Image *close(int size, int threshold = 0, image::Image *mask = nullptr); + + /** + * @brief Returns the image difference of the image and Image.open()’ed image. + * @param size Kernel size. The actual kernel size is ((size * 2) + 1) * ((size * 2) + 1). Use 1(3x3 kernel), 2(5x5 kernel). + * @param threshold As the threshold for open method. default is 0. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.top_hat + */ + image::Image *top_hat(int size, int threshold = 0, image::Image *mask = nullptr); + + /** + * @brief Returns the image difference of the image and Image.close()’ed image. + * @param size Kernel size. The actual kernel size is ((size * 2) + 1) * ((size * 2) + 1). Use 1(3x3 kernel), 2(5x5 kernel). + * @param threshold As the threshold for close method. default is 0. + * @param mask Mask is another image to use as a pixel level mask for the operation. The mask should be an image with just black or white pixels and should be the same size as the image being operated on. + * Only pixels set in the mask are modified. default is None. + * @return Returns the image after the operation is completed. + * @maixpy maix.image.Image.black_hat + */ + image::Image *black_hat(int size, int threshold = 0, image::Image *mask = nullptr); + + /** + * Finds all blobs in the image and returns a list of image.Blob class which describe each Blob. + * Please see the image.Blob object more more information. + * + * @param thresholds You can define multiple thresholds. + * For GRAYSCALE format, you can use {{Lmin, Lmax}, ...} to define one or more thresholds. + * For RGB888 format, you can use {{Lmin, Lmax, Amin, Amax, Bmin, Bmax}, ...} to define one or more thresholds. + * Where the upper case L,A,B represent the L,A,B channels of the LAB image format, and min, max represent the minimum and maximum values of the corresponding channels. + * @param invert if true, will invert thresholds before find blobs, default is false + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @param x_stride x stride is the number of x pixels to skip when doing the hough transform. default is 2 + * @param y_stride y_stride is the number of y pixels to skip when doing the hough transform. default is 1 + * @param area_threshold area threshold, if the blob area is smaller than area_threshold, the blob is not returned, default is 10 + * @param pixels_threshold pixels threshold, if the blob pixels is smaller than area_threshold, the blob is not returned,, default is 10. + * when x_stride and y_stride is equal to 1, pixels_threshold is equivalent to area_threshold + * @param merge if True merges all not filtered out blobs whos bounding rectangles intersect each other. default is false + * @param margin margin can be used to increase or decrease the size of the bounding rectangles for blobs during the intersection test. + * For example, with a margin of 1 blobs whos bounding rectangles are 1 pixel away from each other will be merged. default is 0 + * @param x_hist_bins_max if set to non-zero populates a histogram buffer in each blob object with an x_histogram projection of all columns in the object. This value then sets the number of bins for that projection. + * @param y_hist_bins_max if set to non-zero populates a histogram buffer in each blob object with an y_histogram projection of all rows in the object. This value then sets the number of bins for that projection. + * + * @return Return the blob when found blobs, format is (blob1, blob2, ...), you can use blob class methods to do more operations. + * @maixpy maix.image.Image.find_blobs + */ + std::vector find_blobs(std::vector> thresholds = std::vector>(), bool invert = false, std::vector roi = std::vector(), int x_stride = 2, int y_stride = 1, int area_threshold = 10, int pixels_threshold = 10, bool merge = false, int margin = 0, int x_hist_bins_max = 0, int y_hist_bins_max = 0); + + /** + * Find lines in image + * + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @param x_stride x stride is the number of x pixels to skip when doing the hough transform. default is 2 + * @param y_stride y_stride is the number of y pixels to skip when doing the hough transform. default is 1 + * @param threshold threshold threshold controls what lines are detected from the hough transform. Only lines with a magnitude greater than or equal to threshold are returned. + * The right value of threshold for your application is image dependent. default is 1000. + * @param theta_margin theta_margin controls the merging of detected lines. default is 25. + * @param rho_margin rho_margin controls the merging of detected lines. default is 25. + * + * @return Return the line when found lines, format is (line1, line2, ...), you can use line class methods to do more operations + * @maixpy maix.image.Image.find_lines + */ + std::vector find_lines(std::vector roi = std::vector(), int x_stride = 2, int y_stride = 1, double threshold = 1000, double theta_margin = 25, double rho_margin = 25); + + /** + * @brief Finds all line segments in the image. + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @param merge_distance The maximum distance between two lines to merge them. default is 0. + * @param max_theta_difference The maximum difference between two lines to merge them. default is 15. + * @return Return the line when found lines, format is (line1, line2, ...), you can use line class methods to do more operations + * @maixpy maix.image.Image.find_line_segments + */ + std::vector find_line_segments(std::vector roi = std::vector(), int merge_distance = 0, int max_theta_difference = 15); + + /** + * Find circles in image + * + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @param x_stride x stride is the number of x pixels to skip when doing the hough transform. default is 2 + * @param y_stride y_stride is the number of y pixels to skip when doing the hough transform. default is 1 + * @param threshold threshold controls what circles are detected from the hough transform. Only circles with a magnitude greater than or equal to threshold are returned. + * The right value of threshold for your application is image dependent. + * @param x_margin x_margin controls the merging of detected circles. Circles which are x_margin, y_margin, and r_margin pixels apart are merged. default is 10 + * @param y_margin y_margin controls the merging of detected circles. Circles which are x_margin, y_margin, and r_margin pixels apart are merged. default is 10 + * @param r_margin r_margin controls the merging of detected circles. Circles which are x_margin, y_margin, and r_margin pixels apart are merged. default is 10 + * @param r_min r_min controls the minimum circle radius detected. Increase this to speed up the algorithm. default is 2 + * @param r_max r_max controls the maximum circle radius detected. Decrease this to speed up the algorithm. default is min(roi.w / 2, roi.h / 2) + * @param r_step r_step controls how to step the radius detection by. default is 2. + * + * @return Return the circle when found circles, format is (circle1, circle2, ...), you can use circle class methods to do more operations + * @maixpy maix.image.Image.find_circles + */ + std::vector find_circles(std::vector roi = std::vector(), int x_stride = 2, int y_stride = 1, int threshold = 2000, int x_margin = 10, int y_margin = 10, int r_margin = 10, int r_min = 2, int r_max = -1, int r_step = 2); + + /** + * @brief Finds all rects in the image. + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @param threshold The threshold to use for the rects. default is 10000. + * @return Returns the rects of the image + * @maixpy maix.image.Image.find_rects + */ + std::vector find_rects(std::vector roi = std::vector(), int threshold = 10000); + + /** + * @brief Finds all qrcodes in the image. + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @return Returns the qrcodes of the image + * @maixpy maix.image.Image.find_qrcodes + */ + std::vector find_qrcodes(std::vector roi = std::vector()); + + /** + * @brief Finds all apriltags in the image. + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @param families The families to use for the apriltags. default is TAG36H11. + * @param fx The camera X focal length in pixels, default is -1. + * @param fy The camera Y focal length in pixels, default is -1. + * @param cx The camera X center in pixels, default is image.width / 2. + * @param cy The camera Y center in pixels, default is image.height / 2. + * @return Returns the apriltags of the image + * @maixpy maix.image.Image.find_apriltags + */ + std::vector find_apriltags(std::vector roi = std::vector(), image::ApriltagFamilies families = image::ApriltagFamilies::TAG36H11, float fx = -1, float fy = -1, int cx = -1, int cy = -1); + + /** + * @brief Finds all datamatrices in the image. + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @param effort Controls how much time to spend trying to find data matrix matches. default is 200. + * @return Returns the datamatrices of the image + * @maixpy maix.image.Image.find_datamatrices + */ + std::vector find_datamatrices(std::vector roi = std::vector(), int effort = 200); + + /** + * @brief Finds all barcodes in the image. + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @return Returns the barcodes of the image + * @maixpy maix.image.Image.find_barcodes + */ + std::vector find_barcodes(std::vector roi = std::vector()); + + /** + * @brief Finds the displacement between the image and the template. TODO: support in the feature + * note: this method must be used on power-of-2 image sizes + * @param template_image The template image. + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @param template_roi The region-of-interest rectangle (x, y, w, h) to work in. If not specified, it is equal to the image rectangle. + * @param logpolar If true, it will instead find rotation and scale changes between the two images. default is false. + * @return Returns the displacement of the image + * @maixpy maix.image.Image.find_displacement + */ + image::Displacement find_displacement(image::Image &template_image, std::vector roi = std::vector(), std::vector template_roi = std::vector(), bool logpolar = false); + + /** + * @brief Finds the template in the image. + * @param template_image The template image. + * @param threshold Threshold is floating point number (0.0-1.0) where a higher threshold prevents false positives while lowering the detection rate while a lower threshold does the opposite. + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. Only valid in SEARCH_EX mode. + * @param step The step size to use for the template. default is 2. Only valid in SEARCH_EX mode + * @param search The search method to use for the template. default is SEARCH_EX. + * @return Returns a bounding box tuple (x, y, w, h) for the matching location otherwise None. + * @maixpy maix.image.Image.find_template + */ + std::vector find_template(image::Image &template_image, float threshold, std::vector roi = std::vector(), int step = 2, image::TemplateMatch search = image::TemplateMatch::SEARCH_EX); + + /** + * @brief Finds the features in the image. TODO: support in the feature + * @param cascade The cascade to use for the features. default is CASCADE_FRONTALFACE_ALT. + * @param threshold The threshold to use for the features. default is 0.5. + * @param scale The scale to use for the features. default is 1.5. + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @return Returns the features of the image + * @maixpy maix.image.Image.find_features + */ + std::vector find_features(int cascade, float threshold = 0.5, float scale = 1.5, std::vector roi = std::vector()); + + /** + * @brief Finds the lbp in the image. TODO: support in the feature. + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @return Returns the lbp of the image + * @maixpy maix.image.Image.find_lbp + */ + image::LBPKeyPoint find_lbp(std::vector roi = std::vector()); + + /** + * @brief Finds the keypoints in the image. TODO: support in the feature. + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @param threshold The threshold to use for the keypoints. default is 20. + * @param normalized If true, the image will be normalized before the operation. default is false. + * @param scale_factor The scale factor to use for the keypoints. default is 1.5. + * @param max_keypoints The maximum number of keypoints to use for the keypoints. default is 100. + * @param corner_detector The corner detector to use for the keypoints. default is CORNER_AGAST. + * @return Returns the keypoints of the image + * @maixpy maix.image.Image.find_keypoints + */ + image::ORBKeyPoint find_keypoints(std::vector roi = std::vector(), int threshold = 20, bool normalized = false, float scale_factor = 1.5, int max_keypoints = 100, image::CornerDetector corner_detector = image::CornerDetector::CORNER_AGAST); + + /** + * @brief Finds the edges in the image. + * @param edge_type The edge type to use for the edges. default is EDGE_CANNY. + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @param threshold The threshold to use for the edges. default is 20. + * @return Returns the edges of the image + * @maixpy maix.image.Image.find_edges + */ + image::Image* find_edges(image::EdgeDetector edge_type, std::vector roi = std::vector(), std::vector threshold = std::vector({100, 200})); + + /** + * @brief Finds the hog in the image. TODO: support in the feature + * @param roi The region of interest, input in the format of (x, y, w, h), x and y are the coordinates of the upper left corner, w and h are the width and height of roi. + * default is None, means whole image. + * @param size The size to use for the hog. default is 8. + * @return Returns the hog of the image + * @maixpy maix.image.Image.find_hog + */ + image::Image* find_hog(std::vector roi = std::vector(), int size = 8); + + /** + * @brief Matches the lbp descriptor of the image. TODO: support in the feature + * @param desc1 The descriptor to use for the match. + * @param desc2 The descriptor to use for the match. + * @return Returns the match of the image + * @maixpy maix.image.Image.match_lbp_descriptor + */ + int match_lbp_descriptor(image::LBPKeyPoint &desc1, image::LBPKeyPoint &desc2); + + /** + * @brief Matches the orb descriptor of the image. TODO: support in the feature + * @param desc1 The descriptor to use for the match. + * @param desc2 The descriptor to use for the match. + * @param threshold The threshold to use for the match. default is 95. + * @param filter_outliers If true, the image will be filter_outliers before the operation. default is false. + * @return Returns the match of the image + * @maixpy maix.image.Image.match_orb_descriptor + */ + image::KPTMatch match_orb_descriptor(image::ORBKeyPoint &desc1, image::ORBKeyPoint &desc2, int threshold = 95, bool filter_outliers = false); + + /** + * map point position or rectangle position from this image size to another image size(resize) + * @param int w_out target image width + * @param int h_out target image height + * @param fit resize method, see maix.image.Fit + * @param x original point x, or rectagle left-top point's x + * @param y original point y, or rectagle left-top point's y + * @param w original rectagle width, can be -1 if not use this arg, default -1. + * @param h original rectagle height, can be -1 if not use this arg, default -1. + * @return list type, [x, y] if map point, [x, y, w, h] if resize rectangle. + * @maixpy maix.image.resize_map_pos + */ + std::vector resize_map_pos(int w_out, int h_out, image::Fit fit, int x, int y, int w = -1, int h = -1) + { + return image::resize_map_pos(_width, _height, w_out, h_out, fit, x, y, w, h); + } + + private: + void *_actual_data; + void *_data; + int _width; + int _height; + int _data_size; + Format _format; + bool _is_malloc; + + int _get_cv_pixel_num(image::Format &format); + std::vector _get_available_roi(std::vector roi, std::vector other_roi = std::vector()); + void _create_image(int width, int height, image::Format format, uint8_t *data, int data_size, bool copy); + }; // class Image + + /** + * Load image from file, and convert to Image object + * @param path image file path + * @param format read as this format, if not match, will convert to this format, by default is RGB888 + * @return Image object, if load failed, will return None(nullptr in C++), so you should care about it. + * @maixpy maix.image.load + */ + image::Image *load(const char *path, image::Format format = image::Format::FMT_RGB888); + + /** + * Create image from bytes + * @param width image width + * @param height image height + * @param format image format + * @param data image data, if data is None, will malloc memory for image data + * If the image is in jpeg format, data must be filled in. + * @param copy if true and data is not None, will copy data to new buffer, else will use data directly. default is true to avoid memory leak. + * Use it carefully!!! + * @return Image object + * @maixpy maix.image.from_bytes + */ + image::Image *from_bytes(int width, int height, image::Format format, Bytes *data, bool copy = true); + + /** + * Load font from file + * @param name font name, used to identify font + * @param path font file path, support ttf, ttc, otf + * @param size font size, font height, by default is 16 + * @return error code, err::ERR_NONE is ok, other is error + * @maixpy maix.image.load_font + */ + err::Err load_font(const std::string &name, const char *path, int size = 16); + + /** + * Set default font, if not call this method, default is hershey_plain + * @param name font name, supported names can be get by fonts() + * @return error code, err::ERR_NONE is ok, other is error + * @maixpy maix.image.set_default_font + */ + err::Err set_default_font(const std::string &name); + + /** + * Get all loaded fonts + * @return all loaded fonts, string list type + * @maixpy maix.image.fonts + */ + std::vector *fonts(); + + /** + * Get text rendered width and height + * @param string text content + * @param scale font scale, by default(value is 1) + * @param thickness text thickness(line width), by default(value is 1) + * @return text rendered width and height, [width, height] + * @maixpy maix.image.string_size + */ + image::Size string_size(std::string string, float scale = 1, int thickness = 1, const std::string &font = ""); +} // namespace maix::image diff --git a/vision/components/vision/include/maix_image_color.hpp b/vision/components/vision/include/maix_image_color.hpp new file mode 100644 index 0000000..18de6f0 --- /dev/null +++ b/vision/components/vision/include/maix_image_color.hpp @@ -0,0 +1,296 @@ +/** + * @author neucrack@sipeed, lxowalle@sipeed + * @copyright Sipeed Ltd 2023- + * @license Apache 2.0 + * @update 2023.9.8: Add framework, create this file. + */ + +#pragma once + +#include "maix_image_def.hpp" +#include "maix_log.hpp" + +namespace maix::image +{ + /** + * Color class + * @maixpy maix.image.Color + */ + class Color + { + public: + /** + * Color constructor + * @param alpha alpha channel, value range: 0 ~ 1 + * @maixpy maix.image.Color.__init__ + */ + Color(uint8_t ch1, uint8_t ch2 = 0, uint8_t ch3 = 0, float alpha = 0, image::Format format = image::FMT_GRAYSCALE) + { + if(alpha > 1 || alpha < 0) + throw std::runtime_error("alpha value range: 0 ~ 1"); + this->format = format; + switch (format) + { + case image::FMT_RGB888: + r = ch1; + g = ch2; + b = ch3; + this->alpha = 1; + break; + case image::FMT_BGR888: + b = ch1; + g = ch2; + r = ch3; + this->alpha = 1; + break; + case image::FMT_GRAYSCALE: + gray = ch1; + break; + case image::FMT_BGRA8888: + b = ch1; + g = ch2; + r = ch3; + this->alpha = alpha; + break; + case image::FMT_RGBA8888: + r = ch1; + g = ch2; + b = ch3; + this->alpha = alpha; + break; + default: + throw std::runtime_error("not support format"); + break; + } + } + + /** + * Color red channel + * @maixpy maix.image.Color.r + */ + uint8_t r; + + /** + * Color green channel + * @maixpy maix.image.Color.g + */ + uint8_t g; + + /** + * Color blue channel + * @maixpy maix.image.Color.b + */ + uint8_t b; + + /** + * Color alpha channel, value from 0.0 to 1.0, float value + * @maixpy maix.image.Color.alpha + */ + float alpha; + + /** + * Color gray channel + * @maixpy maix.image.Color.gray + */ + uint8_t gray; + + /** + * Color format + * @maixpy maix.image.Color.format + */ + image::Format format; + + /** + * Get color's hex value + * @maixpy maix.image.Color.hex + */ + uint32_t hex() + { + uint32_t hex = 0; + switch (format) + { + case image::FMT_RGB888: + hex = r | (g << 8) | (b << 16); + break; + case image::FMT_BGR888: + hex = b | (g << 8) | (r << 16); + break; + case image::FMT_GRAYSCALE: + hex = gray; + break; + case image::FMT_BGRA8888: + hex = b | (g << 8) | (r << 16) | ((uint8_t)(alpha*255) << 24); + break; + case image::FMT_RGBA8888: + hex = r | (g << 8) | (b << 16) | ((uint8_t)(alpha*255) << 24); + break; + default: + throw std::runtime_error("not support format"); + break; + } + return hex; + } + + /** + * Create Color object from RGB channels + * @maixpy maix.image.Color.from_rgb + */ + static image::Color from_rgb(uint8_t r, uint8_t g, uint8_t b) + { + return Color(r, g, b, 1, image::Format::FMT_RGB888); + } + + /** + * Create Color object from BGR channels + * @maixpy maix.image.Color.from_bgr + */ + static image::Color from_bgr(uint8_t b, uint8_t g, uint8_t r) + { + return Color(b, g, r, 1, image::Format::FMT_BGR888); + } + + /** + * Create Color object from gray channel + * @maixpy maix.image.Color.from_gray + */ + static image::Color from_gray(uint8_t gray) + { + return Color(gray); + } + + /** + * Create Color object from RGBA channels + * @param alpha alpha channel, float value, value range: 0 ~ 1 + * @maixpy maix.image.Color.from_rgba + */ + static image::Color from_rgba(uint8_t r, uint8_t g, uint8_t b, float alpha) + { + return Color(r, g, b, alpha, image::Format::FMT_RGBA8888); + } + + /** + * Create Color object from BGRA channels + * * @param alpha alpha channel, float value, value range: 0 ~ 1 + * @maixpy maix.image.Color.from_bgra + */ + static image::Color from_bgra(uint8_t b, uint8_t g, uint8_t r, float alpha) + { + return Color(b, g, r, alpha, image::Format::FMT_BGRA8888); + } + + /** + * Create Color object from hex value + * @param hex hex value, e.g. 0x0000FF00, lower address if first channel + * @param format color format, @see image::Format + * @maixpy maix.image.Color.from_hex + */ + static image::Color from_hex(uint32_t hex, image::Format &format) + { + return Color(hex & 0xFF, hex & 0xFF00, hex & 0xFF0000, (hex & 0xFF000000)/255.0, format); + } + + /** + * Convert Color format + * @param format format want to convert to, @see image::Format, only support RGB888, BGR888, RGBA8888, BGRA8888, GRAYSCALE. + * @maixpy maix.image.Color.to_format + */ + void to_format(const image::Format &format) + { + if(!(format == image::FMT_RGB888 || format == image::FMT_BGR888 || + format == image::FMT_RGBA8888 || format == image::FMT_BGRA8888 || + format == image::FMT_GRAYSCALE)) + { + log::error("convert format failed, not support format %d\n", format); + return; + } + if(this->format == format) + return; + if((this->format == image::FMT_RGB888 || this->format == image::FMT_BGR888) && + (format == image::FMT_RGBA8888 || format == image::FMT_BGRA8888)) + { + this->alpha = 1; + } + else if(this->format == image::FMT_GRAYSCALE && format != image::FMT_GRAYSCALE) + { + this->r = this->gray; + this->g = this->gray; + this->b = this->gray; + this->alpha = 1; + } + else if((this->format == image::FMT_RGBA8888 || this->format == image::FMT_BGRA8888) && + (format == image::FMT_RGB888 || format == image::FMT_BGR888)) + { + this->alpha = 0; + } + else if(this->format != image::FMT_GRAYSCALE && format == image::FMT_GRAYSCALE) + { + this->gray = (this->r + this->g + this->b) / 3; + this->r = this->gray; + this->g = this->gray; + this->b = this->gray; + this->alpha = 0; + } + this->format = format; + } + + /** + * Convert color format and return a new Color object + * @param format format want to convert to, @see image::Format, only support RGB888, BGR888, RGBA8888, BGRA8888, GRAYSCALE. + * @return new Color object, you need to delete it manually in C++. + * @maixpy maix.image.Color.to_format2 + */ + image::Color *to_format2(const image::Format &format) + { + image::Color *color = new image::Color(*this); + color->to_format(format); + return color; + } + }; + + /** + * Predefined color white + * @maixpy maix.image.COLOR_WHITE + */ + const image::Color COLOR_WHITE = image::Color::from_rgb(255, 255, 255); + /** + * Predefined color black + * @maixpy maix.image.COLOR_BLACK + */ + const image::Color COLOR_BLACK = image::Color::from_rgb(0, 0, 0); + /** + * Predefined color red + * @maixpy maix.image.COLOR_RED + */ + const image::Color COLOR_RED = image::Color::from_rgb(255, 0, 0); + /** + * Predefined color green + * @maixpy maix.image.COLOR_GREEN + */ + const image::Color COLOR_GREEN = image::Color::from_rgb(0, 255, 0); + /** + * Predefined color blue + * @maixpy maix.image.COLOR_BLUE + */ + const image::Color COLOR_BLUE = image::Color::from_rgb(0, 0, 255); + /** + * Predefined color yellow + * @maixpy maix.image.COLOR_YELLOW + */ + const image::Color COLOR_YELLOW = image::Color::from_rgb(255, 255, 0); + /** + * Predefined color purple + * @maixpy maix.image.COLOR_PURPLE + */ + const image::Color COLOR_PURPLE = image::Color::from_rgb(143, 0, 255); + /** + * Predefined color orange + * @maixpy maix.image.COLOR_ORANGE + */ + const image::Color COLOR_ORANGE = image::Color::from_rgb(255, 127, 0); + /** + * Predefined color gray + * @maixpy maix.image.COLOR_GRAY + */ + const image::Color COLOR_GRAY = image::Color::from_rgb(127, 127, 127); + +} diff --git a/vision/components/vision/include/maix_image_def.hpp b/vision/components/vision/include/maix_image_def.hpp new file mode 100644 index 0000000..aa84e45 --- /dev/null +++ b/vision/components/vision/include/maix_image_def.hpp @@ -0,0 +1,270 @@ +/** + * @author neucrack@sipeed, lxowalle@sipeed + * @copyright Sipeed Ltd 2023- + * @license Apache 2.0 + * @update 2023.9.8: Add framework, create this file. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace maix::image +{ + + /** + * Image formats + * @attention for developers, update this enum will also need to update the fmt_size in maix_image.cpp + * @maixpy maix.image.Format + */ + enum Format + { + FMT_RGB888 = 0, // RGBRGB...RGB, R at the lowest address + FMT_BGR888, // BGRBGR...BGR, B at the lowest address + FMT_RGBA8888, // RGBARGBA...RGBA, R at the lowest address + FMT_BGRA8888, // BGRABGRA...BGRA, B at the lowest address + FMT_RGB565, + FMT_BGR565, + FMT_YUV422SP, // YYY...UVUVUV...UVUV + FMT_YUV422P, // YYY...UUU...VVV + FMT_YVU420SP, // YYY...VUVUVU...VUVU, NV21 + FMT_YUV420SP, // YYY...UVUVUV...UVUV, NV12 + FMT_YVU420P, // YYY...VVV...UUU + FMT_YUV420P, // YYY...UUU...VVV + FMT_GRAYSCALE, + FMT_UNCOMPRESSED_MAX, + + // compressed format below, not compressed should define upper + FMT_COMPRESSED_MIN, + FMT_JPEG, + FMT_PNG, + FMT_COMPRESSED_MAX, + + FMT_INVALID = 0xFF // format not valid + }; + // enum Format + // { + // FMT_RGB888 = 0, // RGBRGB...RGB, R at the lowest address + // FMT_BGR888 = 1, // BGRBGR...BGR, B at the lowest address + // FMT_RGBA8888 = 2, // RGBARGBA...RGBA, R at the lowest address + // FMT_BGRA8888 = 3, // BGRABGRA...BGRA, B at the lowest address + // FMT_RGB565 = 4, + // FMT_BGR565 = 5, + // FMT_YUV422SP = 6, // YYY...UVUVUV...UVUV + // FMT_YUV422P = 7, // YYY...UUU...VVV + // FMT_YVU420SP = 8, // YYY...VUVUVU...VUVU, NV21 + // FMT_YUV420SP = 9, // YYY...UVUVUV...UVUV, NV12 + // FMT_YVU420P = 10, // YYY...VVV...UUU + // FMT_YUV420P = 11, // YYY...UUU...VVV + // FMT_GRAYSCALE = 12, // YYY... + // FMT_UNCOMPRESSED_MAX, + + // // compressed format below, not compressed should define upper + // FMT_COMPRESSED_MIN = 200, + // FMT_JPEG = 201, + // FMT_PNG = 202, + // FMT_COMPRESSED_MAX, + + // FMT_INVALID = 0xFF // format not valid + // }; + /** + * Image format size in bytes + * @attention It's a copy of this variable in MaixPy, + * so change it in C++ (e.g. update var in hello function) will not take effect the var inMaixPy. + * So we add const for this var to avoid this mistake. + * @maixpy maix.image.fmt_size + */ + const std::vector fmt_size = { + 3, + 3, + 4, + 4, + 2, + 2, + 2, + 2, + 1.5, + 1.5, + 1.5, + 1.5, + 1, + 0, + 0, + 0, + 0, + 0 + }; + + /** + * Image format string + * @maixpy maix.image.fmt_names + */ + const std::vector fmt_names = { + "RGB888", + "BGR888", + "RGBA8888", + "BGRA8888", + "RGB565", + "BGR565", + "YUV422SP", + "YUV422P", + "YVU420SP", + "YUV420SP", + "YVU420P" + "YUV420P", + "GRAYSCALE", + "MAX"}; + + /** + * Image size type + * @maixpy maix.image.Size + */ + class Size + { + public: + /** + * Construct a new Size object + * @param width image width + * @param height image height + * @maixpy maix.image.Size.__init__ + */ + Size(int width = 0, int height = 0) + { + this->_width = width; + this->_height = height; + } + + /** + * width of size + * @param width set new width, if not set, only return current width + * @maixpy maix.image.Size.width + */ + int width(int width = -1) + { + if(width != -1) + { + this->_width = width; + } + return this->_width; + } + + /** + * height of size + * @param height set new height, if not set, only return current height + * @maixpy maix.image.Size.height + */ + int height(int height = -1) + { + if(height != -1) + { + this->_height = height; + } + return this->_height; + } + + /** + * Subscript operator + * @param index 0 for width, 1 for height + * @return int& width or height + * @maixpy maix.image.Size.__getitem__ + * @maixcdk maix.image.Size.[] + */ + int &operator[](int index) + { + if (index == 0) + return _width; + else if (index == 1) + return _height; + else + throw std::out_of_range("Size index out of range"); + } + + /** + * to string + * @maixpy maix.image.Size.__str__ + */ + std::string __str__() + { + return "Size(" + std::to_string(_width) + "x" + std::to_string(_height) + ")"; + } + private: + int _width; + int _height; + }; + + /** + * Object fit method + * @maixpy maix.image.Fit + */ + enum Fit + { + FIT_NONE = -1, // no object fit, keep original + FIT_FILL = 0, // width to new width, height to new height, may be stretch + FIT_CONTAIN, // keep aspect ratio, fill blank area with black color + FIT_COVER, // keep aspect ratio, crop image to fit new size + FIT_MAX + }; + + /** + * Resize method + * @maixpy maix.image.ResizeMethod + */ + enum ResizeMethod + { + NEAREST = 0, + BILINEAR, + BICUBIC, + AREA, + LANCZOS, + HAMMING, + RESIZE_METHOD_MAX + }; + + /** + * Family of apriltag + * @maixpy maix.image.ApriltagFamilies + */ + enum ApriltagFamilies + { + TAG16H5 = 1, + TAG25H7 = 2, + TAG25H9 = 4, + TAG36H10 = 8, + TAG36H11 = 16, + ARTOOLKIT = 32 + }; + + /** + * Template match method + * @maixpy maix.image.TemplateMatch + */ + enum TemplateMatch + { + SEARCH_EX, // Exhaustive search + SEARCH_DS, // Diamond search + }; + + /** + * CornerDetector class + * @maixpy maix.image.CornerDetector + */ + enum CornerDetector + { + CORNER_FAST, + CORNER_AGAST + }; + + /** + * EdgeDetector class + * @maixpy maix.image.EdgeDetector + */ + enum EdgeDetector + { + EDGE_CANNY, + EDGE_SIMPLE, + }; +} diff --git a/vision/components/vision/include/maix_image_obj.hpp b/vision/components/vision/include/maix_image_obj.hpp new file mode 100644 index 0000000..d5a2a2c --- /dev/null +++ b/vision/components/vision/include/maix_image_obj.hpp @@ -0,0 +1,2543 @@ +/** + * @author neucrack@sipeed, lxowalle@sipeed + * @copyright Sipeed Ltd 2023- + * @license Apache 2.0 + * @update 2023.9.8: Add framework, create this file. + */ + +#pragma once + +#include "maix_err.hpp" +#include +#include + +namespace maix::image +{ + /** + * Line class + * @maixpy maix.image.Line + */ + class Line + { + private: + int _x1; + int _y1; + int _x2; + int _y2; + int _length; + int _magnitude; + int _theta; + int _rho; + + public: + /** + * Line constructor + * + * @param x1 coordinate x1 of the straight line + * @param y1 coordinate y1 of the straight line + * @param x2 coordinate x2 of the straight line + * @param y2 coordinate y2 of the straight line + * @param magnitude magnitude of the straight line after Hough transformation + * @param theta angle of the straight line after Hough transformation + * @param rho p-value of the straight line after Hough transformation + * @maixpy maix.image.Line.__init__ + */ + Line(int x1, int y1, int x2, int y2, int magnitude = 0, int theta = 0, int rho = 0) + { + _x1 = x1; + _y1 = y1; + _x2 = x2; + _y2 = y2; + int x_diff = x2 - x1; + int y_diff = y2 - y1; + _length = (int)sqrtf(x_diff * x_diff + y_diff * y_diff); + _magnitude = magnitude; + _theta = theta; + _rho = rho; + } + ~Line(){}; + + /** + * Subscript operator + * @param index + * [0] get x1 of line + * [1] get y1 of line + * [2] get x2 of line + * [3] get y2 of line + * [4] get length of line + * [5] get magnitude of the straight line after Hough transformation + * [6] get angle of the straight line after Hough transformation (0-179 degrees) + * [7] get p-value of the straight line after Hough transformation + * @return int& + * @maixpy maix.image.Line.__getitem__ + */ + int &__getitem__(int index) + { + switch (index) { + case 0: return _x1; + case 1: return _y1; + case 2: return _x2; + case 3: return _y2; + case 4: return _length; + case 5: return _magnitude; + case 6: return _theta; + case 7: return _rho; + default:throw std::out_of_range("Line index out of range"); + } + } + + /** + * @brief get x1 of line + * @return return x1 of the line, type is int + * @maixpy maix.image.Line.x1 + */ + int x1() + { + return _x1; + }; + + /** + * @brief get y1 of line + * @return return y1 of the line, type is int + * @maixpy maix.image.Line.y1 + */ + int y1() + { + return _y1; + }; + + /** + * @brief get x2 of line + * @return return x2 of the line, type is int + * @maixpy maix.image.Line.x2 + */ + int x2() + { + return _x2; + }; + + /** + * @brief get y2 of line + * @return return y2 of the line, type is int + * @maixpy maix.image.Line.y2 + */ + int y2() + { + return _y2; + }; + + /** + * @brief get length of line + * @return return length of the line, type is int + * @maixpy maix.image.Line.length + */ + int length() + { + return _length; + }; + + /** + * @brief get magnitude of the straight line after Hough transformation + * @return return magnitude, type is int + * @maixpy maix.image.Line.magnitude + */ + int magnitude() + { + return _magnitude; + }; + + /** + * @brief get angle of the straight line after Hough transformation (0-179 degrees) + * @return return angle, type is int + * @maixpy maix.image.Line.theta + */ + int theta() + { + return _theta; + }; + + /** + * @brief get p-value of the straight line after Hough transformation + * @return return p-value, type is int + * @maixpy maix.image.Line.rho + */ + int rho() + { + return _rho; + }; + }; + + /** + * Rect class + * @maixpy maix.image.Rect + */ + class Rect + { + private: + int _x; + int _y; + int _w; + int _h; + int _magnitude; + std::vector> _corners; + public: + /** + * Rect constructor + * @param corners corners of rect + * @param x coordinate x of the straight line + * @param y coordinate y of the straight line + * @param w coordinate w of the straight line + * @param h coordinate h of the straight line + * @param magnitude magnitude of the straight line after Hough transformation + * @maixpy maix.image.Rect.__init__ + */ + Rect(std::vector> &corners, int x, int y, int w, int h, int magnitude = 0) + { + _x = x; + _y = y; + _w = w; + _h = h; + _magnitude = magnitude; + _corners = corners; + } + ~Rect(){}; + + /** + * Subscript operator + * @param index + * [0] get x of rect + * [1] get y of rect + * [2] get w of rect + * [3] get h of rect + * [4] get magnitude of the straight line after Hough transformation + * @return int& + * @maixpy maix.image.Rect.__getitem__ + */ + int &__getitem__(int index) + { + switch (index) { + case 0: return _x; + case 1: return _y; + case 2: return _w; + case 3: return _h; + case 4: return _magnitude; + default:throw std::out_of_range("Rect index out of range"); + } + } + + + /** + * @brief get corners of rect + * @return return the coordinate of the rect. + * @maixpy maix.image.Rect.corners + */ + std::vector> corners() { + return _corners; + } + + /** + * @brief get rectangle of rect + * @return return the rectangle of the rect. format is {x, y, w, h}, type is std::vector + * @maixpy maix.image.Rect.rect + */ + std::vector rect() { + std::vector coord = {_x, _y, _w, _h}; + return coord; + }; + + /** + * @brief get x of rect + * @return return x of the rect, type is int + * @maixpy maix.image.Rect.x + */ + int x() { + return _x; + }; + + /** + * @brief get y of rect + * @return return y of the rect, type is int + * @maixpy maix.image.Rect.y + */ + int y() { + return _y; + } + + /** + * @brief get w of rect + * @return return w of the rect, type is int + * @maixpy maix.image.Rect.w + */ + int w() { + return _w; + } + + /** + * @brief get h of rect + * @return return h of the rect, type is int + * @maixpy maix.image.Rect.h + */ + int h() { + return _h; + } + + /** + * @brief get magnitude of the straight line after Hough transformation + * @return return magnitude, type is int + * @maixpy maix.image.Rect.magnitude + */ + int magnitude() { + return _magnitude; + } + }; + + /** + * circle class + * @maixpy maix.image.Circle + */ + class Circle + { + private: + int _x; + int _y; + int _r; + int _magnitude; + public: + /** + * Circle constructor + * + * @param x coordinate x of the circle + * @param y coordinate y of the circle + * @param r coordinate r of the circle + * @param magnitude coordinate y2 of the straight line + * @maixpy maix.image.Circle.__init__ + */ + Circle(int x, int y, int r, int magnitude) + { + _x = x; + _y = y; + _r = r; + _magnitude = magnitude; + } + ~Circle(){}; + + /** + * Subscript operator + * @param index + * [0] get x of circle + * [1] get y of circle + * [2] get r of circle + * [3] get magnitude of the circle after Hough transformation + * @return int& + * @maixpy maix.image.Circle.__getitem__ + */ + int &__getitem__(int index) + { + switch (index) { + case 0: return _x; + case 1: return _y; + case 2: return _r; + case 3: return _magnitude; + default:throw std::out_of_range("Circle index out of range"); + } + } + + /** + * @brief get x of circle + * @return return x of the circle, type is int + * @maixpy maix.image.Circle.x + */ + int x() + { + return _x; + }; + + /** + * @brief get y of circle + * @return return y of the circle, type is int + * @maixpy maix.image.Circle.y + */ + int y() + { + return _y; + }; + + /** + * @brief get r of circle + * @return return r of the circle, type is int + * @maixpy maix.image.Circle.r + */ + int r() + { + return _r; + }; + + /** + * @brief get magnitude of the circle after Hough transformation + * @return return magnitude, type is int + * @maixpy maix.image.Circle.magnitude + */ + int magnitude() + { + return _magnitude; + }; + }; + + /** + * Blob class + * @maixpy maix.image.Blob + */ + class Blob + { + private: + int _x; + int _y; + int _w; + int _h; + int _cx; + int _cy; + int _rotation; + float _cxf; + float _cyf; + int _pixels; + float _rotation_f; + int _code; + int _count; + int _merge_cnt; + int _perimeter; + int _roundness; + std::vector _x_hist_bins; + std::vector _y_hist_bins; + std::vector> _corners; + std::vector> _mini_corners; + public: + // /** + // * Blob constructor + // * + // * @param rect blob rect, type is + // * @attention this constructor will be optimized in the future + // */ + // Blob(cv::RotatedRect &rect, int merge_cnt = 0) + // { + // cv::Point2f box_points[4]; + // rect.points(box_points); + // _x = box_points[0].x; + // _y = box_points[0].y; + // _w = rect.size.width; + // _h = rect.size.height; + // _cxf = rect.center.x; + // _cyf = rect.center.y; + // _cx = (int)rect.center.x; + // _cy = (int)rect.center.y; + // _rotation_f = rect.angle; + // _rotation = (int)_rotation_f; + // _merge_cnt = merge_cnt; + // for (int i = 0; i < 4; i++) + // { + // _corners.push_back({(int)box_points[i].x, (int)box_points[i].y}); + // } + // } + + /** + * Blob constructor + * + * @param rect blob rect, type is std::vector + * @param corners blob corners, type is std::vector> + * @param mini_corners blob mini_corners, type is std::vector> + * @param cx blob center x, type is float + * @param cy blob center y, type is float + * @param pixels blob pixels, type is int + * @param rotation blob rotation, type is float + * @param code blob code, type is int + * @param count blob count, type is int + * @param perimeter blob perimeter, type is int + * @param roundness blob roundness, type is float + * @param x_hist_bins blob x_hist_bins, type is std::vector + * @param y_hist_bins blob y_hist_bins, type is std::vector + * @maixpy maix.image.Blob.__init__ + */ + Blob(std::vector &rect, std::vector> &corners, std::vector> &mini_corners,float cx, float cy, int pixels, float rotation, int code, int count, int perimeter, float roundness, std::vector &x_hist_bins, std::vector &y_hist_bins) + { + _x = rect[0]; + _y = rect[1]; + _w = rect[2]; + _h = rect[3]; + _cxf = cx; + _cyf = cy; + _cx = (int)cx; + _cy = (int)cy; + _pixels = pixels; + _rotation_f = rotation; + _rotation = (int)_rotation_f; + _code = code; + _count = count; + _perimeter = perimeter; + _roundness = roundness; + _x_hist_bins = x_hist_bins; + _y_hist_bins = y_hist_bins; + _corners = corners; + _mini_corners = mini_corners; + } + + ~Blob(){}; + + /** + * Subscript operator + * @param index + * [0] Returns the blob’s bounding box x coordinate + * [1] Returns the blob’s bounding box y coordinate + * [2] Returns the blob’s bounding box w coordinate + * [3] Returns the blob’s bounding box h coordinate + * [4] Returns the number of pixels that are part of this blob + * [5] Returns the centroid x position of the blob + * [6] Returns the centroid y position of the blob + * @return int& width or height + * @maixpy maix.image.Blob.__getitem__ + */ + int &__getitem__(int index) + { + switch (index) { + case 0: return _x; + case 1: return _y; + case 2: return _w; + case 3: return _h; + case 4: return _pixels; + case 5: return _cx; + case 6: return _cy; + case 7: return _rotation; + default:throw std::out_of_range("Blob index out of range"); + } + } + + /** + * @brief get blob corners + * @return Returns a list of 4 (x,y) tuples of the 4 corners of the object. + * (x0, y0)___________(x1, y1) + * | | + * | | + * | | + * |___________| + * (x3, y3) (x2, y2) + * note: the order of corners may change + * @maixpy maix.image.Blob.corners + */ + std::vector> corners() + { + return _corners; + } + + /** + * @brief get blob mini corners + * @return Returns a list of 4 (x,y) tuples of the 4 corners than bound the min area rectangle of the blob. + * (x0, y0)___________(x1, y1) + * | | + * | | + * | | + * |___________| + * (x3, y3) (x2, y2) + * note: the order of corners may change + * @maixpy maix.image.Blob.mini_corners + */ + std::vector> mini_corners() + { + return _mini_corners; + } + + /** + * @brief get blob rect + * @return Returns the center coordinates and width and height of the rectangle. format is (x, y, w, h) + * w + * (x, y) ___________ + * | | + * | | h + * | | + * |___________| + * + * @maixpy maix.image.Blob.rect + */ + std::vector rect() + { + std::vector rect = {_x, _y, _w, _h}; + return rect; + }; + + /** + * @brief get blob x of the upper left coordinate + * @return Returns the x coordinate of the upper left corner of the rectangle. + * @maixpy maix.image.Blob.x + */ + int x() + { + return _x; + }; + + /** + * @brief get blob y of the upper left coordinate + * @return Returns the y coordinate of the upper left corner of the rectangle. + * @maixpy maix.image.Blob.y + */ + int y() + { + return _y; + }; + + /** + * @brief get blob width + * @return Returns the blob’s bounding box w coordinate + * @maixpy maix.image.Blob.w + */ + int w() + { + return _w; + }; + + /** + * @brief get blob height + * @return Returns the blob’s bounding box h coordinate + * @maixpy maix.image.Blob.h + */ + int h() + { + return _h; + }; + + /** + * @brief get blob pixels + * @return Returns the number of pixels that are part of this blob. + * @maixpy maix.image.Blob.pixels + */ + int pixels() + { + return _pixels; + }; + + /** + * @brief get blob center x + * @return Returns the centroid x position of the blob + * @maixpy maix.image.Blob.cx + */ + int cx() + { + return _cx; + }; + + /** + * @brief get blob center y + * @return Returns the centroid y position of the blob + * @maixpy maix.image.Blob.cy + */ + int cy() + { + return _cy; + }; + + /** + * @brief get blob center x + * @return Returns the centroid x position of the blob + * @maixpy maix.image.Blob.cxf + */ + float cxf() + { + return _cxf; + }; + + /** + * @brief get blob center y + * @return Returns the centroid y position of the blob + * @maixpy maix.image.Blob.cyf + */ + float cyf() + { + return _cyf; + }; + + /** + * @brief get blob rotation + * @return Returns the rotation of the blob in radians (float). If the blob is like a pencil or pen this value will be unique for 0-180 degrees. + * @maixpy maix.image.Blob.rotation + */ + float rotation() + { + return _rotation_f; + }; + + /** + * @brief get blob rotation_rad + * @return Returns the rotation of the blob in radians + * @maixpy maix.image.Blob.rotation_rad + */ + float rotation_rad() + { + return _rotation_f; + }; + + /** + * @brief get blob rotation_deg + * @return Returns the rotation of the blob in degrees. + * @maixpy maix.image.Blob.rotation_deg + */ + int rotation_deg() + { + return _rotation * 180 / 3.1415926; + }; + + /** + * @brief get blob code + * @return Returns a 32-bit binary number with a bit set in it for each color threshold that’s part of this blob + * @maixpy maix.image.Blob.code + */ + int code() { + return _code; + } + + /** + * @brief get blob count + * @return Returns the number of blobs merged into this blob. + * @maixpy maix.image.Blob.count + */ + int count() { + return _count; + } + + /** + * @brief get blob merge_cnt + * @return Returns the number of pixels on this blob’s perimeter. + * @maixpy maix.image.Blob.perimeter + */ + int perimeter() { + return _perimeter; + } + + /** + * @brief get blob roundness + * @return Returns a value between 0 and 1 representing how round the object is + * @maixpy maix.image.Blob.roundness + */ + float roundness() { + return _roundness; + } + + /** + * @brief get blob elongation + * @returnReturns a value between 0 and 1 representing how long (not round) the object is + * @maixpy maix.image.Blob.elongation + */ + float elongation() { + return 1 - _roundness; + } + + /** + * @brief get blob area + * @return Returns the area of the bounding box around the blob + * @maixpy maix.image.Blob.area + */ + int area() + { + return _w * _h; + }; + + /** + * @brief get blob density + * @return Returns the density ratio of the blob + * @maixpy maix.image.Blob.density + */ + float density() { + int area = _w * _h; + if (area == 0) { + return 0; + } + return (float)_pixels / (float)(_w * _h); + } + + /** + * @brief Alias for blob.density() + * @return Returns the density ratio of the blob + * @maixpy maix.image.Blob.extent + */ + float extent() { + return this->density(); + } + + /** + * @brief get blob compactness + * @return Returns the compactness ratio of the blob + * @maixpy maix.image.Blob.compactness + */ + float compactness() { + float perimeter = _perimeter; + if (perimeter == 0) { + return 0; + } + return (_pixels * 4 * 3.1415926) / (perimeter * perimeter); + } + + /** + * @brief get blob solidity + * @return Returns the solidity ratio of the blob + * @maixpy maix.image.Blob.solidity + */ + float solidity() { + int x0, y0, x1, y1, x2, y2, x3, y3; + x0 = _corners[0][0]; + y0 = _corners[0][1]; + x1 = _corners[1][0]; + y1 = _corners[1][1]; + x2 = _corners[2][0]; + y2 = _corners[2][1]; + x3 = _corners[3][0]; + y3 = _corners[3][1]; + float min_area = (((x0 * y1) + (x1 * y2) + (x2 * y3) + (x3 * y0)) - ((y0 * x1) + (y1 * x2) + (y2 * x3) + (y3 * x0))) / 2.0f; + if (min_area == 0) { + return 0; + } + int pixels = _pixels; + float solidity = (float)pixels / min_area; + return solidity > 1 ? 1 : solidity; + } + + /** + * @brief get blob convexity + * @return Returns a value between 0 and 1 representing how convex the object is + * @maixpy maix.image.Blob.convexity + */ + float convexity() { + int x0, y0, x1, y1, x2, y2, x3, y3; + x0 = _corners[0][0]; + y0 = _corners[0][1]; + x1 = _corners[1][0]; + y1 = _corners[1][1]; + x2 = _corners[2][0]; + y2 = _corners[2][1]; + x3 = _corners[3][0]; + y3 = _corners[3][1]; + + float d0 = sqrtf((x0 - x1) * (x0 - x1) + (y0 - y1) * (y0 - y1)); + float d1 = sqrtf((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); + float d2 = sqrtf((x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3)); + float d3 = sqrtf((x3 - x0) * (x3 - x0) + (y3 - y0) * (y3 - y0)); + int perimeter = _perimeter; + if (perimeter == 0) { + return 0; + } + float convexity = (d0 + d1 + d2 + d3) / perimeter; + return convexity > 1 ? convexity : 1; + } + + /** + * @brief get blob x_hist_bins + * @return Returns the x_hist_bins of the blob + * @maixpy maix.image.Blob.x_hist_bins + */ + std::vector x_hist_bins() { + return _x_hist_bins; + } + + /** + * @brief get blob y_hist_bins + * @return Returns the y_hist_bins of the blob + * @maixpy maix.image.Blob.y_hist_bins + */ + std::vector y_hist_bins() { + return _y_hist_bins; + } + + /** + * @brief get blob major_axis_line + * @return Returns a line tuple (x1, y1, x2, y2) of the minor axis of the blob. + * @maixpy maix.image.Blob.major_axis_line + */ + std::vector major_axis_line() { + int x0, y0, x1, y1, x2, y2, x3, y3; + x0 = _corners[0][0]; + y0 = _corners[0][1]; + x1 = _corners[1][0]; + y1 = _corners[1][1]; + x2 = _corners[2][0]; + y2 = _corners[2][1]; + x3 = _corners[3][0]; + y3 = _corners[3][1]; + + int m0x = (x0 + x1) / 2; + int m0y = (y0 + y1) / 2; + int m1x = (x1 + x2) / 2; + int m1y = (y1 + y2) / 2; + int m2x = (x2 + x3) / 2; + int m2y = (y2 + y3) / 2; + int m3x = (x3 + x0) / 2; + int m3y = (y3 + y0) / 2; + + float l0 = sqrtf((m0x - m2x) * (m0x - m2x) + (m0y - m2y) * (m0y - m2y)); + float l1 = sqrtf((m1x - m3x) * (m1x - m3x) + (m1y - m3y) * (m1y - m3y)); + + if (l0 > l1) { + return {m0x, m0y, m2x, m2y}; + } else { + return {m1x, m1y, m3x, m3y}; + } + } + + /** + * @brief get blob minor_axis_line + * @return Returns a line tuple (x1, y1, x2, y2) of the minor axis of the blob. + * @maixpy maix.image.Blob.minor_axis_line + */ + std::vector minor_axis_line() { + int x0, y0, x1, y1, x2, y2, x3, y3; + x0 = _corners[0][0]; + y0 = _corners[0][1]; + x1 = _corners[1][0]; + y1 = _corners[1][1]; + x2 = _corners[2][0]; + y2 = _corners[2][1]; + x3 = _corners[3][0]; + y3 = _corners[3][1]; + + int m0x = (x0 + x1) / 2; + int m0y = (y0 + y1) / 2; + int m1x = (x1 + x2) / 2; + int m1y = (y1 + y2) / 2; + int m2x = (x2 + x3) / 2; + int m2y = (y2 + y3) / 2; + int m3x = (x3 + x0) / 2; + int m3y = (y3 + y0) / 2; + + float l0 = sqrtf((m0x - m2x) * (m0x - m2x) + (m0y - m2y) * (m0y - m2y)); + float l1 = sqrtf((m1x - m3x) * (m1x - m3x) + (m1y - m3y) * (m1y - m3y)); + + if (l0 < l1) { + return {m0x, m0y, m2x, m2y}; + } else { + return {m1x, m1y, m3x, m3y}; + } + } + + /** + * @brief get blob enclosing_circle + * @return Returns a circle tuple (x, y, r) of the circle that encloses the min area rectangle of a blob. + * @maixpy maix.image.Blob.enclosing_circle + */ + std::vector enclosing_circle() { + int x0, y0, x1, y1, x2, y2, x3, y3; + x0 = _corners[0][0]; + y0 = _corners[0][1]; + x1 = _corners[1][0]; + y1 = _corners[1][1]; + x2 = _corners[2][0]; + y2 = _corners[2][1]; + x3 = _corners[3][0]; + y3 = _corners[3][1]; + + int cx = (x0 + x1 + x2 + x3) / 4; + int cy = (y0 + y1 + y2 + y3) / 4; + + float d0 = sqrtf((x0 - cx) * (x0 - cx) + (y0 - cy) * (y0 - cy)); + float d1 = sqrtf((x1 - cx) * (x1 - cx) + (y1 - cy) * (y1 - cy)); + float d2 = sqrtf((x2 - cx) * (x2 - cx) + (y2 - cy) * (y2 - cy)); + float d3 = sqrtf((x3 - cx) * (x3 - cx) + (y3 - cy) * (y3 - cy)); + float d = std::max(d0, std::max(d1, std::max(d2, d3))); + + return {cx, cy, (int)d}; + } + + /** + * @brief get blob enclosed_ellipse + * @return Returns an ellipse tuple (x, y, rx, ry, rotation) of the ellipse that fits inside of the min area rectangle of a blob. + * @maixpy maix.image.Blob.enclosed_ellipse + */ + std::vector enclosed_ellipse() { + int x0, y0, x1, y1, x2, y2, x3, y3; + x0 = _corners[0][0]; + y0 = _corners[0][1]; + x1 = _corners[1][0]; + y1 = _corners[1][1]; + x2 = _corners[2][0]; + y2 = _corners[2][1]; + x3 = _corners[3][0]; + y3 = _corners[3][1]; + + int m0x = (x0 + x1) / 2; + int m0y = (y0 + y1) / 2; + int m1x = (x1 + x2) / 2; + int m1y = (y1 + y2) / 2; + int m2x = (x2 + x3) / 2; + int m2y = (y2 + y3) / 2; + int m3x = (x3 + x0) / 2; + int m3y = (y3 + y0) / 2; + + int cx = (x0 + x1 + x2 + x3) / 4; + int cy = (y0 + y1 + y2 + y3) / 4; + + float d0 = sqrtf((m0x - cx) * (m0x - cx) + (m0y - cy) * (m0y - cy)); + float d1 = sqrtf((m1x - cx) * (m1x - cx) + (m1y - cy) * (m1y - cy)); + float d2 = sqrtf((m2x - cx) * (m2x - cx) + (m2y - cy) * (m2y - cy)); + float d3 = sqrtf((m3x - cx) * (m3x - cx) + (m3y - cy) * (m3y - cy)); + float a = std::min(d0, d2); + float b = std::min(d1, d3); + + float l0 = sqrtf((m0x - m2x) * (m0x - m2x) + (m0y - m2y) * (m0y - m2y)); + float l1 = sqrtf((m1x - m3x) * (m1x - m3x) + (m1y - m3y) * (m1y - m3y)); + + float r; + if (l0 >= l1) { + if (m0x - m2x == 0) + r = 0; + else + r = atan2f(m0y - m2y, m0x - m2x); + } else { + if (m1x - m3x == 0) + r = 3.1415926 / 2; + else + r = atan2f(m1y - m3y, m1x - m3x) + 3.1415926 / 2; + } + return {cx, cy, (int)a, (int)b, (int)r}; + } + }; // class Blob + + /** + * QRCode class + * @maixpy maix.image.QRCode + */ + class QRCode + { + private: + int _x; + int _y; + int _w; + int _h; + std::string _payload; + int _version; + int _ecc_level; + int _mask; + int _data_type; + int _eci; + std::vector> _corners; + public: + /** + * QRCode constructor + * + * @param rect rect of corners, type is std::vector + * @param corners corners of QRCode + * @param payload payload of the QRCode + * @param version version of the QRCode + * @param ecc_level ecc_level of the QRCode + * @param mask mask of the QRCode + * @param data_type data_type of the QRCode + * @param eci eci of the QRCode + * @maixpy maix.image.QRCode.__init__ + */ + QRCode(std::vector &rect, std::vector> &corners, std::string &payload, int version, int ecc_level, int mask, int data_type, int eci) + { + _x = rect[0]; + _y = rect[1]; + _w = rect[2]; + _h = rect[3]; + _payload = payload; + _version = version; + _ecc_level = ecc_level; + _mask = mask; + _data_type = data_type; + _eci = eci; + _corners = corners; + } + + ~QRCode(){}; + + /** + * Subscript operator + * @param index + * [0] Returns the qrcode’s bounding box x coordinate + * [1] Returns the qrcode’s bounding box y coordinate + * [2] Returns the qrcode’s bounding box w coordinate + * [3] Returns the qrcode’s bounding box h coordinate + * [4] Not support this index, try to use payload() method + * [5] Returns the version of qrcode + * [6] Returns the error correction level of qrcode + * [7] Returns the mask of qrcode + * [8] Returns the datatype of qrcode + * [9] Returns the eci of qrcode + * @return int& + * @maixpy maix.image.QRCode.__getitem__ + */ + int &__getitem__(int index) + { + switch (index) { + case 0: return _x; + case 1: return _y; + case 2: return _w; + case 3: return _h; + case 4: throw err::Exception("Not support this index, try to use payload() method"); + case 5: return _version; + case 6: return _ecc_level; + case 7: return _mask; + case 8: return _data_type; + case 9: return _eci; + default:throw std::out_of_range("QRcode index out of range"); + } + } + + /** + * @brief get coordinate of QRCode + * @return return the coordinate of the QRCode. + * @maixpy maix.image.QRCode.corners + */ + std::vector> corners() { + return _corners; + } + + /** + * @brief get rectangle of QRCode + * @return return the rectangle of the QRCode. format is {x, y, w, h}, type is std::vector + * @maixpy maix.image.QRCode.rect + */ + std::vector rect() { + std::vector coord = {_x, _y, _w, _h}; + return coord; + } + + /** + * @brief get x of QRCode + * @return return x of the QRCode, type is int + * @maixpy maix.image.QRCode.x + */ + int x() { + return _x; + } + + /** + * @brief get y of QRCode + * @return return y of the QRCode, type is int + * @maixpy maix.image.QRCode.y + */ + int y() { + return _y; + } + + /** + * @brief get w of QRCode + * @return return w of the QRCode, type is int + * @maixpy maix.image.QRCode.w + */ + int w() { + return _w; + } + + /** + * @brief get h of QRCode + * @return return h of the QRCode, type is int + * @maixpy maix.image.QRCode.h + */ + int h() { + return _h; + } + + /** + * @brief get QRCode payload + * @return return area of the QRCode + * @maixpy maix.image.QRCode.payload + */ + std::string payload() { + return _payload; + } + + /** + * @brief get QRCode version + * @return return version of the QRCode + * @maixpy maix.image.QRCode.version + */ + int version() { + return _version; + } + + /** + * @brief get QRCode error correction level + * @return return error correction level of the QRCode + * @maixpy maix.image.QRCode.ecc_level + */ + int ecc_level() { + return _ecc_level; + } + + /** + * @brief get QRCode mask + * @return return mask of the QRCode + * @maixpy maix.image.QRCode.mask + */ + int mask() { + return _mask; + } + + /** + * @brief get QRCode dataType + * @return return mask of the QRCode + * @maixpy maix.image.QRCode.data_type + */ + int data_type() { + return _data_type; + } + + /** + * @brief get QRCode eci + * @return return data of the QRCode + * @maixpy maix.image.QRCode.eci + */ + int eci() { + return _eci; + } + + /** + * @brief check QRCode is numeric + * @return return true if the result type of the QRCode is numeric + * @maixpy maix.image.QRCode.is_numeric + */ + bool is_numeric() { + return _data_type == 1; + } + + /** + * @brief check QRCode is alphanumeric + * @return return true if the result type of the QRCode is alphanumeric + * @maixpy maix.image.QRCode.is_alphanumeric + */ + bool is_alphanumeric() { + return _data_type == 2; + } + + /** + * @brief check QRCode is binary + * @return return true if the result type of the QRCode is binary + * @maixpy maix.image.QRCode.is_binary + */ + bool is_binary() { + return _data_type == 4; + } + + /** + * @brief check QRCode is kanji + * @return return true if the result type of the QRCode is kanji + * @maixpy maix.image.QRCode.is_kanji + */ + bool is_kanji() { + return _data_type == 8; + } + }; // class QRCode + + /** + * AprilTag class + * @maixpy maix.image.AprilTag + */ + class AprilTag + { + private: + int _x; + int _y; + int _w; + int _h; + int _id; + std::vector> _corners; + int _family; + float _cx; + float _cy; + float _rotation; + float _decision_margin; + int _hamming; + float _goodness; + float _x_translation; + float _y_translation; + float _z_translation; + float _x_rotation; + float _y_rotation; + float _z_rotation; + public: + /** + * AprilTag constructor + * + * @param rect Inlucdes the top-left corner and the width and height of the rectangle. format is {x, y, w, h}, type is std::vector + * @param corners Includes the four corners of the rectangle. format is {{x0, y0}, {x1, y1}, {x2, y2}, {x3, y3}}, type is std::vector> + * @param id The id of the AprilTag + * @param famliy The family of the AprilTag + * @param centroid_x The x coordinate of the center of the AprilTag + * @param centroid_y The y coordinate of the center of the AprilTag + * @param rotation The rotation of the AprilTag + * @param decision_margin The decision_margin of the AprilTag + * @param hamming The hamming of the AprilTag + * @param goodness The goodness of the AprilTag + * @param x_translation The x_translation of the AprilTag + * @param y_translation The y_translation of the AprilTag + * @param z_translation The z_translation of the AprilTag + * @param x_rotation The x_rotation of the AprilTag + * @param y_rotation The y_rotation of the AprilTag + * @param z_rotation The z_rotation of the AprilTag + * @maixpy maix.image.AprilTag.__init__ + */ + AprilTag(std::vector &rect, std::vector> &corners, int id, int famliy, float centroid_x, float centroid_y, float rotation, float decision_margin, int hamming, float goodness, float x_translation, float y_translation, float z_translation, float x_rotation, float y_rotation, float z_rotation) + { + _x = rect[0]; + _y = rect[1]; + _w = rect[2]; + _h = rect[3]; + _corners = corners; + _id = id; + _family = famliy; + _cx = centroid_x; + _cy = centroid_y; + _rotation = rotation; + _decision_margin = decision_margin; + _hamming = hamming; + _goodness = goodness; + _x_translation = x_translation; + _y_translation = y_translation; + _z_translation = z_translation; + _x_rotation = x_rotation; + _y_rotation = y_rotation; + _z_rotation = z_rotation; + } + + ~AprilTag(){}; + + /** + * Subscript operator + * @param index + * [0] Returns the apriltag’s bounding box x coordinate + * [1] Returns the apriltag’s bounding box y coordinate + * [2] Returns the apriltag’s bounding box w coordinate + * [3] Returns the apriltag’s bounding box h coordinate + * [4] Returns the apriltag’s id + * [5] Returns the apriltag’s family + * [6] Not support + * [7] Not support + * [8] Not support + * [9] Not support + * [10] Returns the apriltag’s hamming + * [11] Not support + * [12] Not support + * [13] Not support + * [14] Not support + * [15] Not support + * [16] Not support + * [17] Not support + * @return int& + * @maixpy maix.image.AprilTag.__getitem__ + */ + int &__getitem__(int index) + { + switch (index) { + case 0: return _x; + case 1: return _y; + case 2: return _w; + case 3: return _h; + case 4: return _id; + case 5: return _family; + case 6: throw err::Exception("Not support this index, try to use cxf() method"); + case 7: throw err::Exception("Not support this index, try to use cyf() method"); + case 8: throw err::Exception("Not support this index, try to use rotation() method"); + case 9: throw err::Exception("Not support this index, try to use decision_margin() method"); + case 10: return _hamming; + case 11: throw err::Exception("Not support this index, try to use goodness() method"); + case 12: throw err::Exception("Not support this index, try to use x_translation() method"); + case 13: throw err::Exception("Not support this index, try to use y_translation() method"); + case 14: throw err::Exception("Not support this index, try to use z_translation() method"); + case 15: throw err::Exception("Not support this index, try to use x_rotation() method"); + case 16: throw err::Exception("Not support this index, try to use y_rotation() method"); + case 17: throw err::Exception("Not support this index, try to use z_rotation() method"); + default:throw std::out_of_range("Apriltag index out of range"); + } + } + + /** + * @brief get coordinate of AprilTag + * @return return the coordinate of the AprilTag. + * @maixpy maix.image.AprilTag.corners + */ + std::vector> corners() { + return _corners; + } + + /** + * @brief get rectangle of AprilTag + * @return return the rectangle of the AprilTag. format is {x, y, w, h}, type is std::vector + * @maixpy maix.image.AprilTag.rect + */ + std::vector rect() { + return {_x, _y, _w, _h}; + } + + /** + * @brief get x of AprilTag + * @return return x of the AprilTag, type is int + * @maixpy maix.image.AprilTag.x + */ + int x() { + return _x; + } + + /** + * @brief get y of AprilTag + * @return return y of the AprilTag, type is int + * @maixpy maix.image.AprilTag.y + */ + int y() { + return _y; + } + + /** + * @brief get w of AprilTag + * @return return w of the AprilTag, type is int + * @maixpy maix.image.AprilTag.w + */ + int w() { + return _w; + } + + /** + * @brief get h of AprilTag + * @return return h of the AprilTag, type is int + * @maixpy maix.image.AprilTag.h + */ + int h() { + return _h; + } + + /** + * @brief get id of AprilTag + * @return return id of the AprilTag, type is int + * @maixpy maix.image.AprilTag.id + */ + int id() { + return _id; + } + + /** + * @brief get family of AprilTag + * @return return family of the AprilTag, type is int + * @maixpy maix.image.AprilTag.family + */ + int family() { + return _family; + } + + /** + * @brief get cx of AprilTag + * @return return cx of the AprilTag, type is int + * @maixpy maix.image.AprilTag.cx + */ + int cx() { + return (int)_cx; + } + + /** + * @brief get cxf of AprilTag + * @return return cxf of the AprilTag, type is float + * @maixpy maix.image.AprilTag.cxf + */ + float cxf() { + return _cx; + } + + /** + * @brief get cy of AprilTag + * @return return cy of the AprilTag, type is int + * @maixpy maix.image.AprilTag.cy + */ + int cy() { + return (int)_cy; + } + + /** + * @brief get cyf of AprilTag + * @return return cyf of the AprilTag, type is float + * @maixpy maix.image.AprilTag.cyf + */ + float cyf() { + return _cy; + } + + /** + * @brief get rotation of AprilTag + * @return return rotation of the AprilTag, type is float + * @maixpy maix.image.AprilTag.rotation + */ + float rotation() { + return _rotation; + } + + /** + * @brief Get decision_margin of AprilTag + * @return Returns the quality of the apriltag match (0.0 - 1.0) where 1.0 is the best. + * @maixpy maix.image.AprilTag.decision_margin + */ + float decision_margin() { + return _decision_margin; + } + + /** + * @brief get hamming of AprilTag + * @return Returns the number of accepted bit errors for this tag. + * return 0, means 0 bit errors will be accepted. + * 1 is TAG25H7, means up to 1 bit error may be accepted + * 2 is TAG25H9, means up to 3 bit errors may be accepted + * 3 is TAG36H10, means up to 3 bit errors may be accepted + * 4 is TAG36H11, means up to 4 bit errors may be accepted + * 5 is ARTOOLKIT, means 0 bit errors will be accepted + * @maixpy maix.image.AprilTag.hamming + */ + int hamming() { + return _hamming; + } + + /** + * @brief get goodness of AprilTag + * @return return goodness of the AprilTag, type is float + * Note: This value is always 0.0 for now. + * @maixpy maix.image.AprilTag.goodness + */ + float goodness() { + return _goodness; + } + + /** + * @brief get x_translation of AprilTag + * @return return x_translation of the AprilTag, type is float + * @maixpy maix.image.AprilTag.x_translation + */ + float x_translation() { + return _x_translation; + } + + /** + * @brief get y_translation of AprilTag + * @return return y_translation of the AprilTag, type is float + * @maixpy maix.image.AprilTag.y_translation + */ + float y_translation() { + return _y_translation; + } + + /** + * @brief get z_translation of AprilTag + * @return return z_translation of the AprilTag, type is float + * @maixpy maix.image.AprilTag.z_translation + */ + float z_translation() { + return _z_translation; + } + + /** + * @brief get x_rotation of AprilTag + * @return return x_rotation of the AprilTag, type is float + * @maixpy maix.image.AprilTag.x_rotation + */ + float x_rotation() { + return _x_rotation; + } + + /** + * @brief get y_rotation of AprilTag + * @return return y_rotation of the AprilTag, type is float + * @maixpy maix.image.AprilTag.y_rotation + */ + float y_rotation() { + return _y_rotation; + } + + /** + * @brief get z_rotation of AprilTag + * @return return z_rotation of the AprilTag, type is float + * @maixpy maix.image.AprilTag.z_rotation + */ + float z_rotation() { + return _z_rotation; + } + }; + + /** + * DataMatrix class + * @maixpy maix.image.DataMatrix + */ + class DataMatrix + { + private: + int _x; + int _y; + int _w; + int _h; + std::vector> _corners; + std::string _payload; + float _rotation; + int _rows; + int _columns; + int _capacity; + int _padding; + public: + /** + * DataMatrix constructor + * + * @param rect Inlucdes the top-left corner and the width and height of the rectangle. format is {x, y, w, h}, type is std::vector + * @param corners Includes the four corners of the rectangle. format is {{x0, y0}, {x1, y1}, {x2, y2}, {x3, y3}}, type is std::vector> + * @param payload The payload of the DataMatrix + * @param rotation The rotation of the DataMatrix + * @param rows The rows of the DataMatrix + * @param columns The columns of the DataMatrix + * @param capacity The capacity of the DataMatrix + * @param padding The padding of the DataMatrix + * @maixpy maix.image.DataMatrix.__init__ + */ + DataMatrix(std::vector &rect, std::vector> &corners, std::string &payload, float rotation, int rows, int columns, int capacity, int padding) + { + _x = rect[0]; + _y = rect[1]; + _w = rect[2]; + _h = rect[3]; + _corners = corners; + _payload = payload; + _rotation = rotation; + _rows = rows; + _columns = columns; + _capacity = capacity; + _padding = padding; + } + + ~DataMatrix(){}; + + /** + * Subscript operator + * @param index + * [0] get x of DataMatrix + * [1] get y of DataMatrix + * [2] get w of DataMatrix + * [3] get h of DataMatrix + * [4] Not support this index, try to use payload() method + * [5] Not support this index, try to use rotation() method + * [6] get rows of DataMatrix + * [7] get columns of DataMatrix + * [8] get capacity of DataMatrix + * [9] get padding of DataMatrix + * @return int& + * @maixpy maix.image.DataMatrix.__getitem__ + */ + int &__getitem__(int index) + { + switch (index) { + case 0: return _x; + case 1: return _y; + case 2: return _w; + case 3: return _h; + case 4: throw err::Exception("Not support this index, try to use payload() method"); + case 5: throw err::Exception("Not support this index, try to use ratation() method"); + case 6: return _rows; + case 7: return _columns; + case 8: return _capacity; + case 9: return _padding; + default:throw std::out_of_range("DataMatrix index out of range"); + } + } + + /** + * @brief get coordinate of DataMatrix + * @return return the coordinate of the DataMatrix. + * @maixpy maix.image.DataMatrix.corners + */ + std::vector> corners() { + return _corners; + } + + /** + * @brief get rectangle of DataMatrix + * @return return the rectangle of the DataMatrix. format is {x, y, w, h}, type is std::vector + * @maixpy maix.image.DataMatrix.rect + */ + std::vector rect() { + return {_x, _y, _w, _h}; + } + + /** + * @brief get x of DataMatrix + * @return return x of the DataMatrix, type is int + * @maixpy maix.image.DataMatrix.x + */ + int x() { + return _x; + } + + /** + * @brief get y of DataMatrix + * @return return y of the DataMatrix, type is int + * @maixpy maix.image.DataMatrix.y + */ + int y() { + return _y; + } + + /** + * @brief get w of DataMatrix + * @return return w of the DataMatrix, type is int + * @maixpy maix.image.DataMatrix.w + */ + int w() { + return _w; + } + + /** + * @brief get h of DataMatrix + * @return return h of the DataMatrix, type is int + * @maixpy maix.image.DataMatrix.h + */ + int h() { + return _h; + } + + /** + * @brief get payload of DataMatrix + * @return return payload of the DataMatrix, type is std::string + * @maixpy maix.image.DataMatrix.payload + */ + std::string payload() { + return _payload; + } + + /** + * @brief get rotation of DataMatrix + * @return return rotation of the DataMatrix, type is float + * @maixpy maix.image.DataMatrix.rotation + */ + float rotation() { + return _rotation; + } + + /** + * @brief get rows of DataMatrix + * @return return rows of the DataMatrix, type is int + * @maixpy maix.image.DataMatrix.rows + */ + int rows() { + return _rows; + } + + /** + * @brief get columns of DataMatrix + * @return return columns of the DataMatrix, type is int + * @maixpy maix.image.DataMatrix.columns + */ + int columns() { + return _columns; + } + + /** + * @brief get capacity of DataMatrix + * @return returns how many characters could fit in this data matrix, type is int + * @maixpy maix.image.DataMatrix.capacity + */ + int capacity() { + return _capacity; + } + + /** + * @brief get padding of DataMatrix + * @return returns how many unused characters are in this data matrix, type is int + * @maixpy maix.image.DataMatrix.padding + */ + int padding() { + return _padding; + } + }; + + /** + * BarCode class + * @maixpy maix.image.BarCode + */ + class BarCode { + private: + int _x; + int _y; + int _w; + int _h; + std::vector> _corners; + std::string _payload; + int _type; + float _rotation; + int _quality; + public: + /** + * BarCode constructor + * + * @param rect Inlucdes the top-left corner and the width and height of the rectangle. format is {x, y, w, h}, type is std::vector + * @param corners Includes the four corners of the rectangle. format is {{x0, y0}, {x1, y1}, {x2, y2}, {x3, y3}}, type is std::vector> + * @param payload The payload of the BarCode + * @param type The type of the BarCode + * @param rotation The rotation of the BarCode + * @param quality The quality of the BarCode + * @maixpy maix.image.BarCode.__init__ + */ + BarCode(std::vector &rect, std::vector> &corners, std::string &payload, int type, float rotation, int quality) + { + _x = rect[0]; + _y = rect[1]; + _w = rect[2]; + _h = rect[3]; + _corners = corners; + _payload = payload; + _type = type; + _rotation = rotation; + _quality = quality; + } + + ~BarCode(){}; + + /** + * Subscript operator + * @param index + * [0] get x of BarCode + * [1] get y of BarCode + * [2] get w of BarCode + * [3] get h of BarCode + * [4] Not support this index, try to use payload() method + * [5] get type of BarCode + * [6] Not support this index, try to use rotation() method + * [7] get quality of BarCode + * @return int& + * @maixpy maix.image.BarCode.__getitem__ + */ + int &__getitem__(int index) + { + switch (index) { + case 0: return _x; + case 1: return _y; + case 2: return _w; + case 3: return _h; + case 4: throw err::Exception("Not support this index, try to use payload() method"); + case 5: return _type; + case 6: throw err::Exception("Not support this index, try to use rotation() method"); + case 7: return _quality; + default:throw std::out_of_range("BarCode index out of range"); + } + } + + /** + * @brief get coordinate of BarCode + * @return return the coordinate of the BarCode. + * @maixpy maix.image.BarCode.corners + */ + std::vector> corners() { + return _corners; + } + + /** + * @brief get rectangle of BarCode + * @return return the rectangle of the BarCode. format is {x, y, w, h}, type is std::vector + * @maixpy maix.image.BarCode.rect + */ + std::vector rect() { + return {_x, _y, _w, _h}; + } + + /** + * @brief get x of BarCode + * @return return x of the BarCode, type is int + * @maixpy maix.image.BarCode.x + */ + int x() { + return _x; + } + + /** + * @brief get y of BarCode + * @return return y of the BarCode, type is int + * @maixpy maix.image.BarCode.y + */ + int y() { + return _y; + } + + /** + * @brief get w of BarCode + * @return return w of the BarCode, type is int + * @maixpy maix.image.BarCode.w + */ + int w() { + return _w; + } + + /** + * @brief get h of BarCode + * @return return h of the BarCode, type is int + * @maixpy maix.image.BarCode.h + */ + int h() { + return _h; + } + + /** + * @brief get payload of BarCode + * @return return payload of the BarCode, type is std::string + * @maixpy maix.image.BarCode.payload + */ + std::string payload() { + return _payload; + } + + /** + * @brief get type of BarCode + * @return return type of the BarCode, type is int + * @maixpy maix.image.BarCode.type + */ + int type() { + return _type; + } + + /** + * @brief get rotation of BarCode + * @return return rotation of the BarCode, type is float. FIXME: always return 0.0 + * @maixpy maix.image.BarCode.rotation + */ + float rotation() { + return _rotation; + } + + /** + * @brief get quality of BarCode + * @return return quality of the BarCode, type is int + * @maixpy maix.image.BarCode.quality + */ + int quality() { + return _quality; + } + }; + + /** + * Statistics class + * @maixpy maix.image.Statistics + */ + class Statistics { + private: + int _l_mean; + int _l_median; + int _l_mode; + int _l_std_dev; + int _l_min; + int _l_max; + int _l_lq; + int _l_uq; + + int _a_mean; + int _a_median; + int _a_mode; + int _a_std_dev; + int _a_min; + int _a_max; + int _a_lq; + int _a_uq; + + int _b_mean; + int _b_median; + int _b_mode; + int _b_std_dev; + int _b_min; + int _b_max; + int _b_lq; + int _b_uq; + + image::Format _format; + public: + /** + * Statistics constructor + * + * @param format The statistics source image format + * @param l_statistics The statistics of the L channel. format is {mean, median, mode, std_dev, min, max, lq, uq}, type is std::vector + * @param a_statistics The statistics of the A channel. format is {mean, median, mode, std_dev, min, max, lq, uq}, type is std::vector + * @param b_statistics The statistics of the B channel. format is {mean, median, mode, std_dev, min, max, lq, uq}, type is std::vector + * @maixpy maix.image.Statistics.__init__ + */ + Statistics(image::Format format, std::vector &l_statistics, std::vector &a_statistics, std::vector &b_statistics) + { + err::check_bool_raise((l_statistics.size() == 8) || (a_statistics.size() == 8) || (b_statistics.size() == 8), "statistics size must be 8"); + + _l_mean = l_statistics[0]; + _l_median = l_statistics[1]; + _l_mode = l_statistics[2]; + _l_std_dev = l_statistics[3]; + _l_min = l_statistics[4]; + _l_max = l_statistics[5]; + _l_lq = l_statistics[6]; + _l_uq = l_statistics[7]; + + _a_mean = a_statistics[0]; + _a_median = a_statistics[1]; + _a_mode = a_statistics[2]; + _a_std_dev = a_statistics[3]; + _a_min = a_statistics[4]; + _a_max = a_statistics[5]; + _a_lq = a_statistics[6]; + _a_uq = a_statistics[7]; + + _b_mean = b_statistics[0]; + _b_median = b_statistics[1]; + _b_mode = b_statistics[2]; + _b_std_dev = b_statistics[3]; + _b_min = b_statistics[4]; + _b_max = b_statistics[5]; + _b_lq = b_statistics[6]; + _b_uq = b_statistics[7]; + + _format = format; + } + Statistics(){}; + ~Statistics(){}; + + /** + * Subscript operator + * @param index array index + * @return int& + * @maixpy maix.image.Statistics.__getitem__ + */ + int &__getitem__(int index) + { + switch (index) { + case 0: return _l_mean; + case 1: return _l_median; + case 2: return _l_mode; + case 3: return _l_std_dev; + case 4: return _l_min; + case 5: return _l_max; + case 6: return _l_lq; + case 7: return _l_uq; + case 8: return _a_mean; + case 9: return _a_median; + case 10: return _a_mode; + case 11: return _a_std_dev; + case 12: return _a_min; + case 13: return _a_max; + case 14: return _a_lq; + case 15: return _a_uq; + case 16: return _b_mean; + case 17: return _b_median; + case 18: return _b_mode; + case 19: return _b_std_dev; + case 20: return _b_min; + case 21: return _b_max; + case 22: return _b_lq; + case 23: return _b_uq; + default:throw std::out_of_range("Statistics index out of range"); + } + } + + /** + * @brief get format of Statistics source image + * @return return format of the Statistics source image, type is image::Format + * @maixpy maix.image.Statistics.format + */ + image::Format format() { + return _format; + } + + /** + * @brief get L channel mean + * @return return L channel mean, type is int + * @maixpy maix.image.Statistics.l_mean + */ + int l_mean() { + return _l_mean; + } + + /** + * @brief get L channel median + * @return return L channel median, type is int + * @maixpy maix.image.Statistics.l_median + */ + int l_median() { + return _l_median; + } + + /** + * @brief get L channel mode + * @return return L channel mode, type is int + * @maixpy maix.image.Statistics.l_mode + */ + int l_mode() { + return _l_mode; + } + + /** + * @brief get L channel std_dev + * @return return L channel std_dev, type is int + * @maixpy maix.image.Statistics.l_std_dev + */ + int l_std_dev() { + return _l_std_dev; + } + + /** + * @brief get L channel min + * @return return L channel min, type is int + * @maixpy maix.image.Statistics.l_min + */ + int l_min() { + return _l_min; + } + + /** + * @brief get L channel max + * @return return L channel max, type is int + * @maixpy maix.image.Statistics.l_max + */ + int l_max() { + return _l_max; + } + + /** + * @brief get L channel lq + * @return return L channel lq, type is int + * @maixpy maix.image.Statistics.l_lq + */ + int l_lq() { + return _l_lq; + } + + /** + * @brief get L channel uq + * @return return L channel uq, type is int + * @maixpy maix.image.Statistics.l_uq + */ + int l_uq() { + return _l_uq; + } + + /** + * @brief get A channel mean + * @return return A channel mean, type is int + * @maixpy maix.image.Statistics.a_mean + */ + int a_mean() { + return _a_mean; + } + + /** + * @brief get A channea median + * @return return A channel median, type is int + * @maixpy maix.image.Statistics.a_median + */ + int a_median() { + return _a_median; + } + + /** + * @brief get A channel mode + * @return return A channel mode, type is int + * @maixpy maix.image.Statistics.a_mode + */ + int a_mode() { + return _a_mode; + } + + /** + * @brief get A channel std_dev + * @return return A channel std_dev, type is int + * @maixpy maix.image.Statistics.a_std_dev + */ + int a_std_dev() { + return _a_std_dev; + } + + /** + * @brief get A channel min + * @return return A channel min, type is int + * @maixpy maix.image.Statistics.a_min + */ + int a_min() { + return _a_min; + } + + /** + * @brief get A channel max + * @return return A channel max, type is int + * @maixpy maix.image.Statistics.a_max + */ + int a_max() { + return _a_max; + } + + /** + * @brief get A channel lq + * @return return A channel lq, type is int + * @maixpy maix.image.Statistics.a_lq + */ + int a_lq() { + return _a_lq; + } + + /** + * @brief get A channel uq + * @return return A channel uq, type is int + * @maixpy maix.image.Statistics.a_uq + */ + int a_uq() { + return _a_uq; + } + + /** + * @brief get B channel mean + * @return return B channel mean, type is int + * @maixpy maix.image.Statistics.b_mean + */ + int b_mean() { + return _b_mean; + } + + /** + * @brief get B channea median + * @return return B channel median, type is int + * @maixpy maix.image.Statistics.b_median + */ + int b_median() { + return _b_median; + } + + /** + * @brief get B channel mode + * @return return B channel mode, type is int + * @maixpy maix.image.Statistics.b_mode + */ + int b_mode() { + return _b_mode; + } + + /** + * @brief get B channel std_dev + * @return return B channel std_dev, type is int + * @maixpy maix.image.Statistics.b_std_dev + */ + int b_std_dev() { + return _b_std_dev; + } + + /** + * @brief get B channel min + * @return return B channel min, type is int + * @maixpy maix.image.Statistics.b_min + */ + int b_min() { + return _b_min; + } + + /** + * @brief get B channel max + * @return return B channel max, type is int + * @maixpy maix.image.Statistics.b_max + */ + int b_max() { + return _b_max; + } + + /** + * @brief get B channel lq + * @return return B channel lq, type is int + * @maixpy maix.image.Statistics.b_lq + */ + int b_lq() { + return _b_lq; + } + + /** + * @brief get B channel uq + * @return return B channel uq, type is int + * @maixpy maix.image.Statistics.b_uq + */ + int b_uq() { + return _b_uq; + } + }; + + + /** + * Displacement class + * @maixpy maix.image.Displacement + */ + class Displacement { + private: + float _x_translation; + float _y_translation; + float _rotation; + float _scale; + float _response; + public: + Displacement(){}; + /** + * Displacement constructor + * + * @param x_translation The x_translation of the Displacement + * @param y_translation The y_translation of the Displacement + * @param rotation The rotation of the Displacement + * @param scale The scale of the Displacement + * @param response The response of the Displacement + * @maixpy maix.image.Displacement.__init__ + */ + Displacement(float x_translation, float y_translation, float rotation, float scale, float response) + { + _x_translation = x_translation; + _y_translation = y_translation; + _rotation = rotation; + _scale = scale; + _response = response; + } + + ~Displacement(){}; + + /** + * Subscript operator + * @param index array index + * @return int& + * @maixpy maix.image.Displacement.__getitem__ + */ + int &__getitem__(int index) + { + switch (index) { + case 0: throw err::Exception("Not support this index, try to use x_translation() method"); + case 1: throw err::Exception("Not support this index, try to use y_translation() method"); + case 2: throw err::Exception("Not support this index, try to use rotation() method"); + case 3: throw err::Exception("Not support this index, try to use scale() method"); + case 4: throw err::Exception("Not support this index, try to use response() method"); + default:throw std::out_of_range("Displacement index out of range"); + } + } + + /** + * @brief get x_translation of Displacement + * @return return x_translation of the Displacement, type is float + * @maixpy maix.image.Displacement.x_translation + */ + float x_translation() { + return _x_translation; + } + + /** + * @brief get y_translation of Displacement + * @return return y_translation of the Displacement, type is float + * @maixpy maix.image.Displacement.y_translation + */ + float y_translation() { + return _y_translation; + } + + /** + * @brief get rotation of Displacement + * @return return rotation of the Displacement, type is float + * @maixpy maix.image.Displacement.rotation + */ + float rotation() { + return _rotation; + } + + /** + * @brief get scale of Displacement + * @return return scale of the Displacement, type is float + * @maixpy maix.image.Displacement.scale + */ + float scale() { + return _scale; + } + + /** + * @brief get response of Displacement + * @return return response of the Displacement, type is float + * @maixpy maix.image.Displacement.response + */ + float response() { + return _response; + } + }; + + /** + * LBPKeyPoint class + * @maixpy maix.image.LBPKeyPoint + */ + class LBPKeyPoint { + private: + std::valarray data; + public: + LBPKeyPoint(){}; + + /** + * LBPKeyPoint constructor + * + * @param data The data of the LBPKeyPoint + * @maixpy maix.image.LBPKeyPoint.__init__ + */ + LBPKeyPoint(std::valarray &data) + { + this->data = data; + } + + ~LBPKeyPoint(){}; + + std::valarray get_data() { + return data; + } + }; + + /** + * KeyPoint class + * @maixpy maix.image.KeyPoint + */ + class KeyPoint { + private: + uint16_t _x; + uint16_t _y; + uint16_t _score; + uint16_t _octave; + uint16_t _angle; + uint16_t _matched; + std::vector desc; + public: + /** + * KeyPoint constructor + * + * @param x The x of the KeyPoint + * @param y The y of the KeyPoint + * @param score The score of the KeyPoint + * @param octave The octave of the KeyPoint + * @param angle The angle of the KeyPoint + * @param matched The matched of the KeyPoint + * @param desc The desc of the KeyPoint + * @maixpy maix.image.KeyPoint.__init__ + */ + KeyPoint(uint16_t x, uint16_t y, uint16_t score, uint16_t octave, uint16_t angle, uint16_t matched, std::vector &desc) + { + _x = x; + _y = y; + _score = score; + _octave = octave; + _angle = angle; + _matched = matched; + this->desc = desc; + } + + ~KeyPoint(){}; + }; + + /** + * KPTMatch class + * @maixpy maix.image.KPTMatch + */ + class KPTMatch { + private: + int cx; + int cy; + int x; + int y; + int w; + int h; + int score; + int theta; + int match; + public: + KPTMatch() {} + /** + * KPTMatch constructor + * + * @param cx The cx of the KPTMatch + * @param cy The cy of the KPTMatch + * @param x The x of the KPTMatch + * @param y The y of the KPTMatch + * @param w The w of the KPTMatch + * @param h The h of the KPTMatch + * @param score The score of the KPTMatch + * @param theta The theta of the KPTMatch + * @param match The match of the KPTMatch + * @maixpy maix.image.KPTMatch.__init__ + */ + KPTMatch(int cx, int cy, int x, int y, int w, int h, int score, int theta, int match) + { + this->cx = cx; + this->cy = cy; + this->x = x; + this->y = y; + this->w = w; + this->h = h; + this->score = score; + this->theta = theta; + this->match = match; + } + + ~KPTMatch(){}; + }; + + /** + * ORBKeyPoint class + * @maixpy maix.image.ORBKeyPoint + */ + class ORBKeyPoint { + private: + std::vector data; + int threshold; + bool normalized; + public: + ORBKeyPoint(){}; + /** + * ORBKeyPoint constructor + * + * @param data The data of the ORBKeyPoint + * @param threshold The threshold of the ORBKeyPoint + * @param normalized The normalized of the ORBKeyPoint + * @maixpy maix.image.ORBKeyPoint.__init__ + */ + ORBKeyPoint(std::vector &data, int threshold, bool normalized) + { + this->data = data; + this->threshold = threshold; + this->normalized = normalized; + } + + ~ORBKeyPoint(){}; + + /** + * @brief get data of ORBKeyPoint + * @return return data of the ORBKeyPoint, type is std::vector + * @maixpy maix.image.ORBKeyPoint.get_data + */ + std::vector get_data() { + return data; + } + }; + + /** + * HaarCascade class + * @maixpy maix.image.HaarCascade + */ + class HaarCascade { + private: + int std; // Image standard deviation. + int step; // Image scanning factor. + float threshold; // Detection threshold. + float scale_factor; // Image scaling factor. + int n_stages; // Number of stages in the cascade. + int n_features; // Number of features in the cascade. + int n_rectangles; // Number of rectangles in the cascade. + std::vector window; // Detection window size. + // image::Image *img; // Grayscale image. + std::vector sum; // Integral image. + // sum:[0] width, [1] height, [2] y_offs, [3] x_ratio, [4] y_ratio, [5] uint32_t **data, [6] uint32_t **swap + std::vector ssq; // Squared integral image. + // ssq:[0] width, [1] height, [2] y_offs, [3] x_ratio, [4] y_ratio, [5] uint32_t **data, [6] uint32_t **swap + uint8_t *stages_array; // Number of features per stage. + int16_t *stages_thresh_array; // Stages thresholds. + int16_t *tree_thresh_array; // Features threshold (1 per feature). + int16_t *alpha1_array; // Alpha1 array (1 per feature). + int16_t *alpha2_array; // Alpha2 array (1 per feature). + int8_t *num_rectangles_array; // Number of rectangles per features (1 per feature). + int8_t *weights_array; // Rectangles weights (1 per rectangle). + int8_t *rectangles_array; // Rectangles array. + public: + /** + * HaarCascade constructor + * + * @param data The data of the HaarCascade + * @param threshold The threshold of the HaarCascade + * @param normalized The normalized of the HaarCascade + * @maixpy maix.image.HaarCascade.__init__ + */ + HaarCascade() + { + + } + + ~HaarCascade(){}; + }; +} diff --git a/vision/components/vision/include/maix_vision.hpp b/vision/components/vision/include/maix_vision.hpp new file mode 100644 index 0000000..4040ff5 --- /dev/null +++ b/vision/components/vision/include/maix_vision.hpp @@ -0,0 +1,4 @@ +#include "maix_image.hpp" +#include "maix_display.hpp" +#include "maix_camera.hpp" +#include "maix_video.hpp" diff --git a/vision/components/vision/include_private/maix_image_util.hpp b/vision/components/vision/include_private/maix_image_util.hpp new file mode 100644 index 0000000..c7c85dd --- /dev/null +++ b/vision/components/vision/include_private/maix_image_util.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "maix_image.hpp" +#include "omv.hpp" + +namespace maix::image +{ + /** + * Convert image to openmv image format + * @param image image + * @param imlib_image openmv image + * @return + */ + extern void convert_to_imlib_image(image::Image *image, image_t *imlib_image); + extern void _convert_to_lab_thresholds(std::vector> &in, list_t *out); +} + diff --git a/vision/components/vision/port/maixcam/maix_camera_mmf.hpp b/vision/components/vision/port/maixcam/maix_camera_mmf.hpp new file mode 100644 index 0000000..7de5bd8 --- /dev/null +++ b/vision/components/vision/port/maixcam/maix_camera_mmf.hpp @@ -0,0 +1,317 @@ +/** + * @author lxowalle@sipeed + * @copyright Sipeed Ltd 2023- + * @license Apache 2.0 + * @update 2023.9.8: Add framework, create this file. + */ + + +#pragma once + +#include +#include "maix_err.hpp" +#include "maix_basic.hpp" +#include "maix_log.hpp" +#include "maix_image.hpp" +#include "maix_time.hpp" +#include "maix_camera_base.hpp" +#include "kvm_mmf.hpp" +#include + +static void try_deinit_mmf() +{ + static uint8_t is_called = 0; + if (!is_called) { + mmf_try_deinit(true); + is_called = 1; + } +} + +static void signal_handle(int signal) +{ + const char *signal_msg = NULL; + switch (signal) { + case SIGILL: signal_msg = "SIGILL"; break; + case SIGTRAP: signal_msg = "SIGTRAP"; break; + case SIGABRT: signal_msg = "SIGABRT"; break; + case SIGBUS: signal_msg = "SIGBUS"; break; + case SIGFPE: signal_msg = "SIGFPE"; break; + case SIGKILL: signal_msg = "SIGKILL"; break; + case SIGSEGV: signal_msg = "SIGSEGV"; break; + default: signal_msg = "UNKNOWN"; break; + } + + maix::log::error("Trigger signal, code:%s(%d)!\r\n", signal_msg, signal); + try_deinit_mmf(); + exit(1); +} + +// FIXME: move this function to port/maix_vision_maixcam.cpp ? +static __attribute__((constructor)) void maix_vision_register_signal(void) +{ + signal(SIGILL, signal_handle); + signal(SIGTRAP, signal_handle); + signal(SIGABRT, signal_handle); + signal(SIGBUS, signal_handle); + signal(SIGFPE, signal_handle); + signal(SIGKILL, signal_handle); + signal(SIGSEGV, signal_handle); + + maix::util::register_exit_function(try_deinit_mmf); +} + +namespace maix::camera +{ + class CameraCviMmf final : public CameraBase + { + public: + CameraCviMmf(const std::string device, int width, int height, image::Format format, int buff_num) + { + this->device = device; + this->format = format; + this->width = width; + this->height = height; + this->buffer_num = buff_num; + this->ch = -1; + + if (0 != mmf_init()) { + err::check_raise(err::ERR_RUNTIME, "mmf init failed"); + } + + if (0 != mmf_vi_init()) { + err::check_raise(err::ERR_RUNTIME, "mmf vi init failed"); + } + } + + CameraCviMmf(const std::string device, int ch, int width, int height, image::Format format, int buff_num) + { + this->device = device; + this->format = format; + this->width = width; + this->height = height; + this->buffer_num = buff_num; + this->ch = ch; + + if (0 != mmf_init()) { + err::check_raise(err::ERR_RUNTIME, "mmf init failed"); + } + + if (0 != mmf_vi_init()) { + err::check_raise(err::ERR_RUNTIME, "mmf vi init failed"); + } + } + + ~CameraCviMmf() + { + mmf_del_vi_channel(this->ch); + if (0 != mmf_vi_deinit()) { + err::check_raise(err::ERR_RUNTIME, "mmf vi init failed"); + } + mmf_try_deinit(true); + } + + err::Err open(int width, int height, image::Format format, int buff_num) + { + if (format == image::FMT_GRAYSCALE) { + format = image::FMT_YVU420SP; + } + int ch = mmf_get_vi_unused_channel(); + if (ch < 0) { + log::error("camera open: mmf get vi channel failed"); + return err::ERR_RUNTIME; + } + if (0 != mmf_add_vi_channel(ch, width, height, mmf_invert_format_to_mmf(format))) { + log::error("camera open: mmf add vi channel failed"); + return err::ERR_RUNTIME; + } + + this->ch = ch; + this->width = (width == -1) ? this->width : width; + this->height = (height == -1) ? this->height : height; + this->align_width = mmf_vi_aligned_width(this->ch); + this->align_need = (this->width % this->align_width == 0) ? false : true; // Width need align only + return err::ERR_NONE; + } // open + + bool is_support_format(image::Format format) + { + if(format == image::Format::FMT_RGB888 || format == image::Format::FMT_BGR888 + || format == image::Format::FMT_YVU420SP|| format == image::Format::FMT_GRAYSCALE) + return true; + return false; + } + + void close() + { + if (mmf_vi_chn_is_open(this->ch) == true) { + if (0 != mmf_del_vi_channel(this->ch)) { + log::error("mmf del vi channel failed"); + } + } + } // close + + // read + image::Image *read(void *buff = NULL, size_t buff_size = 0) + { + // printf("Func: maix_image.cpp read\r\n"); + image::Image *img = NULL; + + void *buffer = NULL; + int buffer_len = 0, width = 0, height = 0, format = 0; + + if (0 == mmf_vi_frame_pop(this->ch, &buffer, &buffer_len, &width, &height, &format)) { + if (buffer == NULL) { + mmf_vi_frame_free(this->ch); + printf("mmf_vi_frame_free error\r\n"); + return NULL; + } + if(buff) + { + if(buff_size < (size_t)buffer_len) + { + log::error("camera read: buff size not enough, need %d, but %d", buffer_len, buff_size); + mmf_vi_frame_free(this->ch); + return NULL; + } + img = new image::Image(width, height, this->format, (uint8_t*)buff, buff_size, false); + } + else + { + img = new image::Image(this->width, this->height, this->format); + } + void *image_data = img->data(); + switch (img->format()) { + case image::Format::FMT_GRAYSCALE: + if (this->align_need) { + for (int h = 0; h < this->height; h ++) { + memcpy((uint8_t *)image_data + h * this->width, (uint8_t *)buffer + h * width, this->width); + } + } else { + memcpy(image_data, buffer, this->width * this->height); + } + break; + case image::Format::FMT_BGR888: // fall through + case image::Format::FMT_RGB888: + if (this->align_need) { + for (int h = 0; h < this->height; h++) { + memcpy((uint8_t *)image_data + h * this->width * 3, (uint8_t *)buffer + h * width * 3, this->width * 3); + } + } else { + memcpy(image_data, buffer, this->width * this->height * 3); + } + break; + case image::Format::FMT_YVU420SP: + if (this->align_need) { + for (int h = 0; h < this->height * 3 / 2; h ++) { + memcpy((uint8_t *)image_data + h * this->width, (uint8_t *)buffer + h * width, this->width); + } + } else { + memcpy(image_data, buffer, this->width * this->height * 3 / 2); + } + break; + default: + printf("unknown format\n"); + delete img; + mmf_vi_frame_free(this->ch); + printf("switch (img->format()\r\n"); + return NULL; + } + mmf_vi_frame_free(this->ch); + // printf("mmf_vi_frame_free\r\n"); + return img; + } + + return img; + } // read + + camera::CameraCviMmf *add_channel(int width, int height, image::Format format, int buff_num) + { + int new_channel = mmf_get_vi_unused_channel(); + if (new_channel < 0) { + log::error("Support not more channel!\r\n"); + return NULL; + } + return new camera::CameraCviMmf(this->device, new_channel, width, height, format, buff_num); + } + + void clear_buff() + { + + } + + bool is_opened() { + return mmf_vi_chn_is_open(this->ch); + } + + int get_ch_nums() { + return 1; + } + + int get_channel() { + return this->ch; + } + + int hmirror(int en) + { + bool out; + if (en == -1) { + mmf_get_vi_hmirror(this->ch, &out); + } else { + bool need_open = false; + if (this->is_opened()) { + this->close(); + need_open = true; + } + + mmf_set_vi_hmirror(this->ch, en); + + if (need_open) { + err::check_raise(this->open(this->width, this->height, this->format, this->buffer_num), "Open failed"); + } + out = en; + } + + return out; + } + + int vflip(int en) + { + bool out; + if (en == -1) { + mmf_get_vi_vflip(this->ch, &out); + } else { + bool need_open = false; + if (this->is_opened()) { + this->close(); + need_open = true; + } + + mmf_set_vi_vflip(this->ch, en); + + if (need_open) { + err::check_raise(this->open(this->width, this->height, this->format, this->buffer_num), "Open failed"); + } + out = en; + } + return out; + } + + private: + std::string device; + image::Format format; + int fd; + int ch; + uint32_t raw_format; + std::vector buffers; + std::vector buffers_len; + int buffer_num; + int queue_id; // user directly used buffer id + int width; + int height; + void *buff; + bool buff_alloc; + int align_width; + bool align_need; + }; + +} // namespace maix::camera diff --git a/vision/components/vision/src/maix_camera.cpp b/vision/components/vision/src/maix_camera.cpp new file mode 100644 index 0000000..8dab65c --- /dev/null +++ b/vision/components/vision/src/maix_camera.cpp @@ -0,0 +1,425 @@ +/** + * @author neucrack@sipeed, lxowalle@sipeed + * @copyright Sipeed Ltd 2023- + * @license Apache 2.0 + * @update 2023.9.8: Add framework, create this file. + */ + + +#include "maix_camera.hpp" +#include +#ifdef PLATFORM_LINUX + #include "maix_camera_v4l2.hpp" +#endif +#ifdef PLATFORM_MAIXCAM + #include "maix_camera_mmf.hpp" +#endif + +namespace maix::camera +{ + static bool set_regs_flag = false; + + std::vector list_devices() + { + // find to /dev/video* + std::vector devices; + std::string path = "/dev"; + DIR *dir = opendir(path.c_str()); + if (dir == NULL) + { + return devices; + } + struct dirent *ptr; + while ((ptr = readdir(dir)) != NULL) + { + if (ptr->d_type == DT_CHR) + { + std::string name = ptr->d_name; + if (name.find("video") != std::string::npos) + { + devices.push_back( path + "/" + name ); + } + } + } + closedir(dir); + // sort devices with name + std::sort(devices.begin(), devices.end()); + // print devices + for (size_t i = 0; i < devices.size(); i++) + { + log::debug("find device: %s\n", devices[i].c_str()); + } + return devices; + } + + void set_regs_enable(bool enable) { + set_regs_flag = enable; + } + + err::Err Camera::show_colorbar(bool enable) + { + // only set variable now + // should control camera to show colorbar + _show_colorbar = enable; + return err::ERR_NONE; + } + + static void generate_colorbar(image::Image &img) + { + int width = img.width(); + int height = img.height(); + int step = width / 8; + int color_step = 255 / 7; + int color = 0; + uint8_t colors[8][3] = { + {255, 255, 255}, + {255, 0, 0}, + {255, 127, 0}, + {255, 255, 0}, + {0, 255, 0}, + {0, 0, 255}, + {143, 0, 255}, + {0, 0, 0}, + }; + for (int i = 0; i < 8; i++) + { + image::Color _color(colors[i][0], colors[i][1], colors[i][2], 0, image::FMT_RGB888); + img.draw_rect(i * step, 0, step, height, _color, -1); + color += color_step; + } + } +#ifdef PLATFORM_LINUX + static char * _get_device(const char *device) + { + if (device) + { + return (char *)device; + } + else + { + std::vector devices = list_devices(); + err::check_bool_raise(devices.size() > 0, "No camera device"); + return (char *)devices[0].c_str(); + } + } +#endif + + Camera::Camera(int width, int height, image::Format format, const char *device, int fps, int buff_num, bool open) + { + err::Err e; + err::check_bool_raise(_check_format(format), "Format not support"); + + if (format == image::Format::FMT_RGB888 && width * height * 3 > 640 * 640 * 3) { + log::warn("Note that we do not recommend using large resolution RGB888 images, which can take up a lot of memory!\r\n"); + } + + _width = (width == -1) ? 640 : width; + _height = (height == -1) ? 480 : height; + _format = format; + _fps = (fps == -1) ? 30 : fps; + _buff_num = buff_num; + + _show_colorbar = false; + _open_set_regs = set_regs_flag; + _impl = NULL; + +#ifdef PLATFORM_LINUX + _device = _get_device(device); + _impl = new CameraV4L2(_device, _width, _height, _format, _buff_num); +#endif + +#ifdef PLATFORM_MAIXCAM + _device = ""; + _impl = new CameraCviMmf(_device, _width, _height, _format, _buff_num); +#endif + + if (open) { + e = this->open(_width, _height, _format, _buff_num); + + // err::check_raise(e, "camera open failed"); + } + } + + Camera::Camera(const char *device, CameraBase *base, int width, int height, image::Format format, int fps, int buff_num, bool open) + { + err::Err e; + err::check_bool_raise(_check_format(format), "Format not support"); + + _width = (width == -1) ? 640 : width; + _height = (height == -1) ? 480 : height; + _format = format; + _fps = (fps == -1) ? 30 : fps; + _buff_num = buff_num; + + _show_colorbar = false; + _open_set_regs = set_regs_flag; + _impl = base; + + if (open) { + e = this->open(_width, _height, _format, _buff_num); + err::check_raise(e, "camera open failed"); + } + } + + Camera::~Camera() + { + if (this->is_opened()) { + this->close(); + } +#ifdef PLATFORM_LINUX + delete (CameraV4L2*)_impl; +#endif + +#ifdef PLATFORM_MAIXCAM + delete (CameraCviMmf*)_impl; +#endif + } + + void Camera::restart(int width, int height, image::Format format, const char *device, int fps, int buff_num, bool open) + { + if (this->is_opened()) { + this->close(); + } +#ifdef PLATFORM_LINUX + delete (CameraV4L2*)_impl; +#endif + +#ifdef PLATFORM_MAIXCAM + delete (CameraCviMmf*)_impl; +#endif + + err::Err e; + err::check_bool_raise(_check_format(format), "Format not support"); + + if (format == image::Format::FMT_RGB888 && width * height * 3 > 640 * 640 * 3) { + log::warn("Note that we do not recommend using large resolution RGB888 images, which can take up a lot of memory!\r\n"); + } + + _width = (width == -1) ? 640 : width; + _height = (height == -1) ? 480 : height; + _format = format; + _fps = (fps == -1) ? 30 : fps; + _buff_num = buff_num; + + _show_colorbar = false; + _open_set_regs = set_regs_flag; + _impl = NULL; + +#ifdef PLATFORM_LINUX + _device = _get_device(device); + _impl = new CameraV4L2(_device, _width, _height, _format, _buff_num); +#endif + +#ifdef PLATFORM_MAIXCAM + _device = ""; + _impl = new CameraCviMmf(_device, _width, _height, _format, _buff_num); +#endif + + if (open) { + e = this->open(_width, _height, _format, _buff_num); + + // err::check_raise(e, "camera open failed"); + } + } + + int Camera::get_ch_nums() + { + return 2; + } + + int Camera::get_channel() + { + if (_impl == NULL) + return err::ERR_NOT_INIT; + + if (!this->is_opened()) { + return err::ERR_NOT_OPEN; + } + + return _impl->get_channel(); + } + + bool Camera::_check_format(image::Format format) { + if (format == image::FMT_RGB888 || format == image::FMT_BGR888 + || format == image::FMT_RGBA8888 || format == image::FMT_BGRA8888 + || format == image::FMT_YVU420SP || format == image::FMT_GRAYSCALE) { + return true; + } else { + return false; + } + } + + err::Err Camera::open(int width, int height, image::Format format, int fps, int buff_num) + { + if (_impl == NULL) + return err::Err::ERR_RUNTIME; + + int width_tmp = (width == -1) ? _width : width; + int height_tmp = (height == -1) ? _height : height; + image::Format format_tmp = (format == image::FMT_INVALID) ? _format : format; + int fps_tmp = (fps == -1) ? 30 : fps; + int buff_num_tmp =( buff_num == -1) ? _buff_num : buff_num; + + err::check_bool_raise(_check_format(format_tmp), "Format not support"); + + if (this->is_opened()) { + if (width == width_tmp && height == height_tmp && format == format_tmp && fps == fps_tmp && buff_num == buff_num_tmp) { + return err::ERR_NONE; + } + this->close(); // Get new param, close and reopen + } + + _width = width_tmp; + _height = height_tmp; + _fps = fps_tmp; + _buff_num = buff_num_tmp; + _format = format_tmp; + _format_impl = _format; + if(!_impl->is_support_format(_format)) + { + if(_impl->is_support_format(image::FMT_RGB888)) + _format_impl = image::FMT_RGB888; + else if(_impl->is_support_format(image::FMT_BGR888)) + _format_impl = image::FMT_BGR888; + else if(_impl->is_support_format(image::FMT_YVU420SP)) + _format_impl = image::FMT_YVU420SP; + else if(_impl->is_support_format(image::FMT_YUV420SP)) + _format_impl = image::FMT_YUV420SP; + else if(_impl->is_support_format(image::FMT_RGBA8888)) + _format_impl = image::FMT_RGBA8888; + else if(_impl->is_support_format(image::FMT_BGRA8888)) + _format_impl = image::FMT_BGRA8888; + else if(_impl->is_support_format(image::FMT_GRAYSCALE)) + _format_impl = image::FMT_GRAYSCALE; + else + return err::ERR_ARGS; + } + + return _impl->open(_width, _height, _format_impl, _buff_num);; + } + + void Camera::close() + { + if (this->is_closed()) + return; + + _impl->close(); + } + + camera::Camera *Camera::add_channel(int width, int height, image::Format format, int fps, int buff_num, bool open) + { + err::check_bool_raise(_check_format(format), "Format not support"); + + int width_tmp = (width == -1) ? _width : width; + int height_tmp = (height == -1) ? _height : height; + image::Format format_tmp = (format == image::Format::FMT_INVALID) ? _format : format; + int fps_tmp = (fps == -1) ? _fps : fps; + int buff_num_tmp = buff_num == -1 ? _buff_num : buff_num; + + Camera *cam = NULL; + if (_impl) { + CameraBase *cam_base = _impl->add_channel(width_tmp, height_tmp, format_tmp, buff_num); + err::check_bool_raise(cam_base, "Unable to add a new channel. Please check the maximum number of supported channels."); + cam = new Camera(_device.c_str(), cam_base, width_tmp, height_tmp, format_tmp, fps_tmp, buff_num_tmp, open); + } + return cam; + } + + bool Camera::is_opened() + { + if (_impl == NULL) + return false; + + return _impl->is_opened(); + } + + image::Image *Camera::read(void *buff, size_t buff_size, bool block) + { + if (!this->is_opened()) { + // err::Err e = open(_width, _height, _format, _buff_num); + // err::check_raise(e, "open camera failed"); + return NULL; + } + + if (_show_colorbar) { + image::Image *img = new image::Image(_width, _height); + generate_colorbar(*img); + err::check_null_raise(img, "camera read failed"); + return img; + } else { + // it's better all done by impl to faster read, but if impl not support, we have to convert it + if(_format_impl == _format) + { + image::Image *img = _impl->read(buff, buff_size); + // err::check_null_raise(img, "camera read failed"); + return img; + } + else + { + image::Image *img = _impl->read(); + image::Image *img2 = img->to_format(_format, buff, buff_size); + delete img; + // err::check_null_raise(img2, "camera read failed"); + return img2; + } + } + } + + void Camera::clear_buff() + { + if (_impl == NULL) + return; + _impl->clear_buff(); + } + + void Camera::skip_frames(int num) + { + if (_impl == NULL) + return; + for(int i = 0; i < num; i++) + { + image::Image *img = _impl->read(); + delete img; + } + } + + err::Err Camera::set_resolution(int width, int height) + { + err::Err e; + if (_impl == NULL) + return err::ERR_NOT_INIT; + + if (this->is_opened()) { + this->close(); + } + + _width = width; + _height = height; + e = this->open(_width, _height, _format, _buff_num); + err::check_raise(e, "camera open failed"); + return err::ERR_NONE; + } + + int Camera::hmirror(int value) { + if (_impl == NULL) + return err::ERR_NOT_INIT; + + if (!this->is_opened()) { + return err::ERR_NOT_OPEN; + } + + return _impl->hmirror(value); + } + + int Camera::vflip(int value) { + if (_impl == NULL) + return err::ERR_NOT_INIT; + + if (!this->is_opened()) { + return err::ERR_NOT_OPEN; + } + + return _impl->vflip(value); + } +} + diff --git a/vision/components/vision/src/maix_image.cpp b/vision/components/vision/src/maix_image.cpp new file mode 100644 index 0000000..9405b4a --- /dev/null +++ b/vision/components/vision/src/maix_image.cpp @@ -0,0 +1,1654 @@ +/** + * @author neucrack@sipeed, lxowalle@sipeed + * @copyright Sipeed Ltd 2023- + * @license Apache 2.0 + * @update 2023.9.8: Add framework, create this file. + */ + +#include "maix_image.hpp" +#include "opencv2/opencv.hpp" +#include "opencv2/freetype.hpp" +#include +#include +#include +#include +#include +#ifdef PLATFORM_MAIXCAM +#include "kvm_mmf.hpp" +#endif + +namespace maix::image +{ + + static void _get_cv_format_color(image::Format _format, const image::Color &color_in, int *ch_format, cv::Scalar &cv_color); + + static uint8_t _get_char_size(const uint8_t c) + { + if ((c & 0x80) == 0x00) { + // 0xxxxxxx: 1 byte character + return 1; + } else if ((c & 0xE0) == 0xC0) { + // 110xxxxx: 2 byte character + return 2; + } else if ((c & 0xF0) == 0xE0) { + // 1110xxxx: 3 byte character + return 3; + } else if ((c & 0xF8) == 0xF0) { + // 11110xxx: 4 byte character + return 4; + } else { + // Invalid UTF-8 start byte, default to 1 + return 1; + } + } + + + image::Image *load(const char *path, image::Format format) + { + cv::Mat mat; + if (format == image::FMT_BGR888 || format == image::FMT_RGB888) + { + mat = cv::imread(path); + if (mat.empty()) + return nullptr; + if (format == image::FMT_RGB888) + cv::cvtColor(mat, mat, cv::COLOR_BGR2RGB); + } + else + { + mat = cv::imread(path, cv::IMREAD_UNCHANGED); + if (mat.empty()) + return nullptr; + if (mat.channels() == 1) + { + switch (format) + { + case image::FMT_GRAYSCALE: + break; + case image::FMT_RGB888: + cv::cvtColor(mat, mat, cv::COLOR_GRAY2RGB); + break; + case image::FMT_RGBA8888: + cv::cvtColor(mat, mat, cv::COLOR_GRAY2RGBA); + break; + case image::FMT_BGR888: + cv::cvtColor(mat, mat, cv::COLOR_GRAY2BGR); + break; + case image::FMT_BGRA8888: + cv::cvtColor(mat, mat, cv::COLOR_GRAY2BGRA); + break; + default: + log::error("load image failed, can't convert grayscale to format %d\n", format); + return nullptr; + } + } + else if (mat.channels() == 3) + { + switch (format) + { + case image::FMT_GRAYSCALE: + cv::cvtColor(mat, mat, cv::COLOR_BGR2GRAY); + break; + case image::FMT_RGB888: + cv::cvtColor(mat, mat, cv::COLOR_BGR2RGB); + break; + case image::FMT_RGBA8888: + cv::cvtColor(mat, mat, cv::COLOR_BGR2RGBA); + break; + case image::FMT_BGR888: + break; + case image::FMT_BGRA8888: + cv::cvtColor(mat, mat, cv::COLOR_BGR2BGRA); + break; + default: + log::error("load image failed, can't convert bgr to format %d\n", format); + return nullptr; + } + } + else if (mat.channels() == 4) + { + switch (format) + { + case image::FMT_GRAYSCALE: + cv::cvtColor(mat, mat, cv::COLOR_BGRA2GRAY); + break; + case image::FMT_RGB888: + cv::cvtColor(mat, mat, cv::COLOR_BGRA2RGB); + break; + case image::FMT_RGBA8888: + cv::cvtColor(mat, mat, cv::COLOR_BGRA2RGBA); + break; + case image::FMT_BGR888: + cv::cvtColor(mat, mat, cv::COLOR_BGRA2BGR); + break; + case image::FMT_BGRA8888: + break; + default: + log::error("load image failed, can't convert bgra to format %d\n", format); + return nullptr; + } + } + else + { + log::error("load image failed, channels not support: %d\n", mat.channels()); + return nullptr; + } + } + image::Image *img = new image::Image(mat.cols, mat.rows, format); + memcpy(img->data(), mat.data, mat.cols * mat.rows * mat.channels()); + return img; + } + + image::Image *from_bytes(int width, int height, image::Format format, Bytes *data, bool copy) + { + // _create_image(width, height, format, data->data, data->size(), copy); + return new image::Image(width, height, format, data->data, data->size(), copy); + } + + static std::map> fonts_info; + static std::map fonts_size_info; + static std::string curr_font_name = "hershey_plain"; + static int curr_font_id = cv::FONT_HERSHEY_PLAIN; // -1 if user custom font, else opencv's HersheyFonts id + + static void add_default_fonts(std::map> &fonts_info) + { + if (!fonts_info.empty()) + return; + fonts_info["hershey_simplex"] = cv::Ptr(); + fonts_info["hershey_plain"] = cv::Ptr(); + fonts_info["hershey_duplex"] = cv::Ptr(); + fonts_info["hershey_complex"] = cv::Ptr(); + fonts_info["hershey_triplex"] = cv::Ptr(); + fonts_info["hershey_complex_small"] = cv::Ptr(); + fonts_info["hershey_script_simplex"] = cv::Ptr(); + } + + static int get_default_fonts_id(const std::string &name) + { + if (name == "hershey_simplex") + return cv::FONT_HERSHEY_SIMPLEX; + else if (name == "hershey_plain") + return cv::FONT_HERSHEY_PLAIN; + else if (name == "hershey_duplex") + return cv::FONT_HERSHEY_DUPLEX; + else if (name == "hershey_complex") + return cv::FONT_HERSHEY_COMPLEX; + else if (name == "hershey_triplex") + return cv::FONT_HERSHEY_TRIPLEX; + else if (name == "hershey_complex_small") + return cv::FONT_HERSHEY_COMPLEX_SMALL; + else if (name == "hershey_script_simplex") + return cv::FONT_HERSHEY_SCRIPT_SIMPLEX; + else + return -1; + } + + err::Err load_font(const std::string &name, const char *path, int size) + { + add_default_fonts(fonts_info); + // use opencv to load freetype font and store object in global variable + cv::Ptr ft2 = cv::freetype::createFreeType2(); + if (!ft2) + { + log::error("load font failed\n"); + return err::ERR_ARGS; + } + ft2->loadFontData(path, 0); + fonts_info[name] = ft2; + fonts_size_info[name] = size; + return err::ERR_NONE; + } + + err::Err set_default_font(const std::string &name) + { + // if name in fonts_info + if (fonts_info.find(name) == fonts_info.end()) + { + log::error("font %d not load\n", name.c_str()); + return err::ERR_ARGS; + } + curr_font_name = name; + curr_font_id = get_default_fonts_id(name); + return err::ERR_NONE; + } + + std::vector *fonts() + { + std::vector *fonts = new std::vector; + add_default_fonts(fonts_info); + for (auto &font : fonts_info) + { + fonts->push_back(font.first); + } + return fonts; + } + + static void _get_text_size(cv::Size &size, const std::string &text, const std::string &font_name, int font_id, float scale, int thickness) + { + int baseLine = 0; + if (font_id == -1) + { + int font_height = fonts_size_info[font_name]; + cv::Ptr ft2 = fonts_info[font_name]; + if (ft2 == cv::Ptr()) + { + log::error("font %d not load\n", font_name.c_str()); + throw std::runtime_error("font not load"); + } + size = ft2->getTextSize(text, scale * font_height, thickness, &baseLine); + if (thickness > 0) + baseLine += thickness; + size.height = size.height + baseLine; + } + else + { + size = cv::getTextSize(text, font_id, scale, thickness > 0 ? thickness : -thickness, &baseLine); + baseLine += baseLine > 0 ? 0 : -thickness; + size.height += baseLine; + } + } + + static void _put_text(cv::Mat &img, const std::string &text, const cv::Point &point, + const cv::Scalar &color, float scale, int thickness, const std::string &font_name, int font_id) + { + if (font_id == -1) + { + cv::Ptr ft2 = fonts_info[font_name]; + if (ft2 == cv::Ptr()) + { + log::error("font %d not load\n", font_name.c_str()); + throw std::runtime_error("font not load"); + } + // point from left top to left center + const std::string text_tmp(text, 0, 1); + cv::Point point_tmp(point.x, point.y + ft2->getTextSize(text, scale * fonts_size_info[font_name], thickness, nullptr).height); + ft2->putText(img, text, point_tmp, scale * fonts_size_info[font_name], color, thickness, cv::LINE_AA, true); + } + else + { + // left top corner to point left bottom corner by first char height + const std::string text_tmp(text, 0, 1); + int baseLine = 0; + cv::Point point_tmp(point.x, point.y + cv::getTextSize(text_tmp, font_id, scale, thickness > 0 ? thickness : -thickness, &baseLine).height); + cv::putText(img, text, point_tmp, font_id, scale, color, thickness > 0 ? thickness : -thickness, cv::LINE_AA, false); + } + } + + void Image::_create_image(int width, int height, image::Format format, uint8_t *data, int data_size, bool copy) + { + _format = format; + _width = width; + _height = height; + if (width <= 0 || height <= 0) + throw err::Exception(err::ERR_ARGS, "image width and height should > 0"); + + if (_format > Format::FMT_COMPRESSED_MIN) { + if (!data || data_size < 0) + throw err::Exception(err::ERR_ARGS,"image data and data_size are incorrect"); + _data_size = data_size; + } else { + // not use data_size, uncompressed image only use fiexed size. + int size_calc = width * height * image::fmt_size[format]; + if(data_size > 0 && data_size != size_calc) + { + log::error("data_size not match image content size, data_size: %d, image content size: %d\n", data_size, size_calc); + throw err::Exception(err::ERR_ARGS, "data_size not match image content size"); + } + _data_size = size_calc; + } + + if (!data) + { + _actual_data = malloc(_data_size + 0x1000); + if (!_actual_data) + throw err::Exception(err::ERR_NO_MEM, "malloc image data failed"); + _data = (void *)(((uint64_t)_actual_data + 0x1000) & ~0xFFF); + // log::debug("malloc image data\n"); + _is_malloc = true; + } + else + { + if(!copy) + { + _data = data; + _actual_data = _data; + _is_malloc = false; + } + else + { + _actual_data = malloc(_data_size + 0x1000); + if (!_actual_data) + throw std::bad_alloc(); + _data = (void *)(((uint64_t)_actual_data + 0x1000) & ~0xFFF); + memcpy(_data, data, _data_size); + // log::debug("malloc image data\n"); + _is_malloc = true; + } + } + } + + Image::Image(int width, int height, image::Format format, uint8_t *data, int data_size, bool copy) + { + _create_image(width, height, format, data, data_size, copy); + } + + Image::Image(int width, int height, image::Format format) + // Image::Image(int width, int height, image::Format format, Bytes *data, bool copy) + { + // if(!data) + // { + _create_image(width, height, format, nullptr, 0, false); + // return; + // } + // _create_image(width, height, format, data->data, data->size(), copy); + } + + Image::~Image() + { + if (_is_malloc) + { + // log::debug("free image data\n"); + free(_actual_data); + _actual_data = NULL; + _data = NULL; + } + } + + void Image::operator=(const image::Image &img) + { + _format = img._format; + _width = img._width; + _height = img._height; + _data = malloc(_width * _height * image::fmt_size[_format]); + if (!_data) + throw std::bad_alloc(); + memcpy(_data, img._data, _width * _height * image::fmt_size[_format]); + } + + std::string Image::__str__() + { + char buf[128]; + sprintf(buf, "Image(%d, %d, %s), data size: %d", _width, _height, fmt_names[_format].c_str(), (int)(_width * _height * fmt_size[_format])); + return std::string(buf); + } + + Bytes *Image::to_bytes(bool copy) + { + if(copy) + return new Bytes((uint8_t *)_data, _data_size, true, true); + return new Bytes((uint8_t *)_data, _data_size, false, false); + } + + tensor::Tensor *Image::to_tensor(bool chw, bool copy) + { + void *data = _data; + std::vector shape; + tensor::Tensor *t = nullptr; + if (_format == image::FMT_GRAYSCALE) + shape = {_height, _width}; + else if (_format < image::FMT_YUV422SP) + shape = chw ? (std::vector){(int)image::fmt_size[_format], _height, _width} : (std::vector){_height, _width, (int)image::fmt_size[_format]}; + else if (_format == image::FMT_YUV422SP || _format == image::FMT_YUV422P) + shape = chw ? (std::vector){2, _height, _width} : (std::vector){_height, _width, 2}; + else if (_format == image::FMT_YUV420SP || _format == image::FMT_YUV420P || _format == image::FMT_YVU420SP || _format == image::FMT_YVU420P) + shape = {(int)(_height * 1.5), _width}; + else + throw std::runtime_error("not support format"); + if (copy) + { + t = new tensor::Tensor(shape, tensor::UINT8, nullptr); + memcpy(t->data(), data, t->size_int() * tensor::dtype_size[t->dtype()]); + } + else + { + t = new tensor::Tensor(shape, tensor::UINT8, data); + } + return t; + } + + image::Image *Image::to_format(const image::Format &format, void *buff, size_t buff_size) + { + if (_format == format) + { + log::error("convert format failed, already the format %d\n", format); + throw err::Exception(err::ERR_ARGS, "convert format failed, already the format"); + } + + cv::Mat src(_height, _width, CV_8UC((int)image::fmt_size[_format]), _data); + cv::ColorConversionCodes cvt_code; + if(format == image::FMT_JPEG) // compress + { + switch (_format) { +#ifdef PLATFORM_MAIXCAM + case image::FMT_GRAYSCALE: + { + image::Image *img = nullptr; + if (!mmf_enc_jpg_push_with_quality(0, (uint8_t *)_data, _width, _height, mmf_invert_format_to_mmf(image::FMT_GRAYSCALE), 95)) { + uint8_t *data; + int data_size; + if (!mmf_enc_jpg_pop(0, &data, &data_size)) { + img = new image::Image(_width, _height, format, data, data_size, true); + mmf_enc_jpg_free(0); + } + } + return img; + break; + } + case image::FMT_YVU420SP: + { + image::Image *img = nullptr; + if (!mmf_enc_jpg_push_with_quality(0, (uint8_t *)_data, _width, _height, mmf_invert_format_to_mmf(image::FMT_YVU420SP), 95)) { + uint8_t *data; + int data_size; + if (!mmf_enc_jpg_pop(0, &data, &data_size)) { + img = new image::Image(_width, _height, format, data, data_size, true); + mmf_enc_jpg_free(0); + } + } + return img; + break; + } +#endif + default: + { +#ifdef PLATFORM_MAIXCAM + image::Image *p_img = nullptr; + image::Image *img = nullptr; + bool src_alloc = false; + if(_format != Format::FMT_RGB888) + { + p_img = to_format(image::FMT_RGB888); + src_alloc = true; + } else { + p_img = this; + } + + if (!mmf_enc_jpg_push_with_quality(0, (uint8_t *)p_img->data(), _width, _height, mmf_invert_format_to_mmf(image::FMT_RGB888), 80)) { + uint8_t *data; + int data_size; + if (!mmf_enc_jpg_pop(0, &data, &data_size)) { + img = new image::Image(_width, _height, format, data, data_size, true); + mmf_enc_jpg_free(0); + } + } + + if (src_alloc) + { + delete p_img; + } + return img; +#else + image::Image *p_img = nullptr; + cv::Mat *p_src = &src; + bool src_alloc = false; + if(_format != Format::FMT_BGR888 && _format != Format::FMT_BGRA8888) + { + p_img = to_format(image::FMT_BGR888); + p_src = new cv::Mat(p_img->_height, p_img->_width, CV_8UC((int)image::fmt_size[image::FMT_BGR888]), p_img->_data); + src_alloc = true; + } + cv::InputArray input(*p_src); + std::vector jpeg_buff; + std::vector params; + params.push_back(cv::IMWRITE_JPEG_QUALITY); + params.push_back(95); + cv::imencode(".jpg", input, jpeg_buff, params); + image::Image *img; + if(buff) + { + if(buff_size < jpeg_buff.size()) + { + log::error("convert format failed, buffer size not enough\n"); + throw err::Exception(err::ERR_ARGS, "convert format failed, buffer size not enough"); + } + memcpy(buff, jpeg_buff.data(), jpeg_buff.size()); + img = new image::Image(src.cols, src.rows, format, (uint8_t*)buff, jpeg_buff.size(), false); + } + else + img = new image::Image(src.cols, src.rows, format, (uint8_t*)jpeg_buff.data(), jpeg_buff.size(), true); + if(src_alloc) + { + delete p_img; + delete p_src; + } + return img; +#endif + break; + } + } + + } + else if(format == image::FMT_PNG) + { + image::Image *p_img = nullptr; + cv::Mat *p_src = &src; + bool src_alloc = false; + if(_format != Format::FMT_BGR888 && _format != Format::FMT_BGRA8888) + { + p_img = to_format(image::FMT_BGRA8888); + p_src = new cv::Mat(p_img->_height, p_img->_width, CV_8UC((int)image::fmt_size[image::FMT_BGRA8888]), p_img->_data); + src_alloc = true; + } + cv::InputArray input(*p_src); + std::vector png_buff; + std::vector params; + params.push_back(cv::IMWRITE_PNG_COMPRESSION); + params.push_back(3); + cv::imencode(".png", input, png_buff, params); + image::Image *img; + if(buff) + { + if(buff_size < png_buff.size()) + { + log::error("convert format failed, buffer size not enough\n"); + throw err::Exception(err::ERR_ARGS, "convert format failed, buffer size not enough"); + } + memcpy(buff, png_buff.data(), png_buff.size()); + img = new image::Image(src.cols, src.rows, format, (uint8_t*)buff, png_buff.size(), false); + } + else + img = new image::Image(src.cols, src.rows, format, png_buff.data(), png_buff.size(), true); + if(src_alloc) + { + delete p_img; + delete p_src; + } + return img; + } + switch (_format) + { + case image::FMT_RGB888: + switch (format) + { + case image::FMT_GRAYSCALE: + cvt_code = cv::COLOR_RGB2GRAY; + break; + case image::FMT_BGR888: + cvt_code = cv::COLOR_RGB2BGR; + break; + case image::FMT_RGBA8888: + cvt_code = cv::COLOR_RGB2RGBA; + break; + case image::FMT_BGRA8888: + cvt_code = cv::COLOR_RGB2BGRA; + break; + case image::FMT_YVU420SP: + cvt_code = cv::COLOR_RGB2YUV_YV12; + break; + default: + log::error("convert format failed, can't convert rgb to format %d\n", format); + throw err::Exception(err::ERR_NOT_IMPL, "not support format"); + } + break; + case image::FMT_BGR888: + switch (format) + { + case image::FMT_GRAYSCALE: + cvt_code = cv::COLOR_BGR2GRAY; + break; + case image::FMT_RGB888: + cvt_code = cv::COLOR_BGR2RGB; + break; + case image::FMT_RGBA8888: + cvt_code = cv::COLOR_BGR2RGBA; + break; + case image::FMT_BGRA8888: + cvt_code = cv::COLOR_BGR2BGRA; + break; + case image::FMT_YVU420SP: + cvt_code = cv::COLOR_BGR2YUV_YV12; + break; + default: + throw err::Exception(err::ERR_NOT_IMPL, "not support format"); + } + break; + case image::FMT_RGBA8888: + switch (format) + { + case image::FMT_GRAYSCALE: + cvt_code = cv::COLOR_RGBA2GRAY; + break; + case image::FMT_RGB888: + cvt_code = cv::COLOR_RGBA2RGB; + break; + case image::FMT_BGR888: + cvt_code = cv::COLOR_RGBA2BGR; + break; + case image::FMT_BGRA8888: + cvt_code = cv::COLOR_RGBA2BGRA; + break; + case image::FMT_YVU420SP: + cvt_code = cv::COLOR_BGR2YUV_YV12; + break; + default: + throw err::Exception(err::ERR_NOT_IMPL, "not support format"); + } + break; + case image::FMT_BGRA8888: + switch (format) + { + case image::FMT_GRAYSCALE: + cvt_code = cv::COLOR_BGRA2GRAY; + break; + case image::FMT_RGB888: + cvt_code = cv::COLOR_BGRA2RGB; + break; + case image::FMT_BGR888: + cvt_code = cv::COLOR_BGRA2BGR; + break; + case image::FMT_RGBA8888: + cvt_code = cv::COLOR_BGRA2RGBA; + break; + case image::FMT_YVU420SP: + cvt_code = cv::COLOR_BGR2YUV_YV12; + break; + default: + throw err::Exception(err::ERR_NOT_IMPL, "not support format"); + } + break; + case image::FMT_GRAYSCALE: + switch (format) + { + case image::FMT_RGB888: + cvt_code = cv::COLOR_GRAY2RGB; + break; + case image::FMT_BGR888: + cvt_code = cv::COLOR_GRAY2BGR; + break; + case image::FMT_RGBA8888: + cvt_code = cv::COLOR_GRAY2RGBA; + break; + case image::FMT_BGRA8888: + cvt_code = cv::COLOR_GRAY2BGRA; + break; + case image::FMT_YVU420SP: + cvt_code = cv::COLOR_BGR2YUV_YV12; + break; + default: + throw err::Exception(err::ERR_NOT_IMPL, "not support format"); + } + break; + default: + throw err::Exception(err::ERR_NOT_IMPL, "not support format"); + } + image::Image *img; + if(buff) + { + if(buff_size < image::fmt_size[format]) + { + log::error("convert format failed, buffer size not enough, need %d, but %d\n", image::fmt_size[format], buff_size); + throw err::Exception(err::ERR_ARGS, "convert format failed, buffer size not enough"); + } + img = new image::Image(src.cols, src.rows, format, (uint8_t*)buff, src.cols * src.rows * image::fmt_size[format], false); + } + else + img = new image::Image(src.cols, src.rows, format); + + if (_format == image::FMT_GRAYSCALE && format == image::FMT_YVU420SP) { + cv::Mat dst(src.rows * 3 / 2, src.cols, CV_8UC((int)image::fmt_size[format]), img->data()); + int nv_len = src.cols * src.rows / 2; + int offset = src.cols * src.rows; + memcpy(dst.data, src.data, src.cols * src.rows); + memset(dst.data + offset, 128, nv_len); + } else if (format == image::FMT_YVU420SP) { + cv::Mat dst(src.rows * 3 / 2, src.cols, CV_8UC((int)image::fmt_size[format]), img->data()); + cv::cvtColor(src, dst, cvt_code); + int nv_len = src.cols * src.rows / 2; + int half_len = nv_len / 2; + int offset = src.cols * src.rows; + uint8_t *nv21 = (uint8_t *)img->data(); + uint8_t *uv_temp = (uint8_t *)malloc(nv_len); + err::check_null_raise(uv_temp, "malloc uv_temp failed"); + memcpy(uv_temp, nv21 + offset, nv_len); + for (int i = 0; i < half_len; i ++) { + nv21[offset + i * 2] = uv_temp[i]; // V + nv21[offset + i * 2 + 1] = uv_temp[half_len + i]; // U + } + if (uv_temp) + free(uv_temp); + } else if (_format == image::FMT_RGB888 && format == image::FMT_GRAYSCALE) { + int height = _width; + int width = _height; + uint8_t *src = (uint8_t *)_data; + uint8_t *dst = (uint8_t *)img->data(); + for (int i = 0; i < height; i ++) { + for (int j = 0; j < width; j ++) { + dst[i * width + j] = (src[(i * width + j) * 3 + 0] * 38 + src[(i * width + j) * 3 + 1] * 75 + src[(i * width + j) * 3 + 2] * 15) >> 7; + } + } + } else { + cv::Mat dst(src.rows, src.cols, CV_8UC((int)image::fmt_size[format]), img->data()); + cv::cvtColor(src, dst, cvt_code); + } + + return img; + } + + + image::Image *Image::to_format(const image::Format &format) + { + return to_format(format, nullptr, 0); + } + + image::Image *Image::to_jpeg(int quality) + { + image::Format format = image::Format::FMT_JPEG; + cv::Mat src(_height, _width, CV_8UC((int)image::fmt_size[_format]), _data); + switch (_format) { + case image::FMT_YVU420SP: + { +#ifdef PLATFORM_MAIXCAM + image::Image *img = nullptr; + if (!mmf_enc_jpg_push_with_quality(0, (uint8_t *)_data, _width, _height, mmf_invert_format_to_mmf(image::FMT_YVU420SP), quality)) { + uint8_t *data; + int data_size; + if (!mmf_enc_jpg_pop(0, &data, &data_size)) { + img = new image::Image(_width, _height, format, data, data_size, true); + mmf_enc_jpg_free(0); + } + } + return img; +#endif + break; + } + default: + { +#ifdef PLATFORM_MAIXCAM + image::Image *p_img = nullptr; + image::Image *img = nullptr; + bool src_alloc = false; + if(_format != Format::FMT_RGB888) + { + p_img = to_format(image::FMT_RGB888); + src_alloc = true; + } else { + p_img = this; + } + + if (!mmf_enc_jpg_push_with_quality(0, (uint8_t *)p_img->data(), _width, _height, mmf_invert_format_to_mmf(image::FMT_RGB888), quality)) { + uint8_t *data; + int data_size; + if (!mmf_enc_jpg_pop(0, &data, &data_size)) { + img = new image::Image(_width, _height, format, data, data_size, true); + mmf_enc_jpg_free(0); + } + } + + if (src_alloc) + { + delete p_img; + } + return img; +#else + image::Image *p_img = nullptr; + cv::Mat *p_src = &src; + bool src_alloc = false; + if(_format != Format::FMT_BGR888 && _format != Format::FMT_BGRA8888) + { + p_img = to_format(image::FMT_BGR888); + p_src = new cv::Mat(p_img->_height, p_img->_width, CV_8UC((int)image::fmt_size[image::FMT_BGR888]), p_img->_data); + src_alloc = true; + } + cv::InputArray input(*p_src); + std::vector jpeg_buff; + std::vector params; + params.push_back(cv::IMWRITE_JPEG_QUALITY); + params.push_back(60); // 压缩率 + cv::imencode(".jpg", input, jpeg_buff, params); + image::Image *img; + img = new image::Image(src.cols, src.rows, format, (uint8_t*)jpeg_buff.data(), jpeg_buff.size(), true); + if(src_alloc) + { + delete p_img; + delete p_src; + } + return img; +#endif + break; + } + } + + return nullptr; + } + + image::Image *Image::draw_image(int x, int y, image::Image &img) + { + image::Format fmt = img.format(); + if (!(fmt == image::FMT_GRAYSCALE || fmt == image::FMT_RGB888 || fmt == image::FMT_BGR888 || + fmt == image::FMT_RGBA8888 || fmt == image::FMT_BGRA8888)) + throw std::runtime_error("image format not support"); + // check format + if (image::fmt_size[img.format()] > image::fmt_size[_format]) + throw std::runtime_error("image format not match"); + cv::Mat src(img.height(), img.width(), CV_8UC((int)image::fmt_size[img.format()]), img.data()); + cv::Mat dst(_height, _width, CV_8UC((int)image::fmt_size[_format]), _data); + cv::Rect rect(x, y, img.width(), img.height()); + if (_format == fmt && image::fmt_size[fmt] < 4) + { + src.copyTo(dst(rect)); + } + else + { + // convert format + bool img_alloc = false; + image::Image *img_new = nullptr; + if (_format != fmt) + { + img_new = img.to_format(_format); + if (!img_new) + { + return img_new; + } + img_alloc = true; + } + else + img_new = &img; + cv::Mat src_new(img_new->height(), img_new->width(), CV_8UC((int)image::fmt_size[_format]), img_new->data()); + if (image::fmt_size[_format] < 4) + { + src_new.copyTo(dst(rect)); + } + else // merge two BGRA images + { + for (int y = 0; y < src_new.rows; y++) + { + if(y + rect.y >= dst.rows) + break; + for (int x = 0; x < src_new.cols; x++) + { + if(x + rect.x >= dst.cols) + break; + cv::Vec4b &pixel = src_new.at(y, x); + // ignore transparent pixel + if (pixel[3] == 0) + continue; + cv::Vec4b &pixel_dst = dst.at(y + rect.y, x + rect.x); + // alpha is 255, just copy + if (pixel[3] == 255) + { + pixel_dst[0] = pixel[0]; + pixel_dst[1] = pixel[1]; + pixel_dst[2] = pixel[2]; + pixel_dst[3] = pixel[3]; + } + // alpha > 0 and < 255, blend + else + { + // TODO: optimize blend algorithm + pixel_dst[0] = (uint32_t)((pixel[0] * pixel[3] + pixel_dst[0] * (255 - pixel[3]))) >> 8; + pixel_dst[1] = (uint32_t)((pixel[1] * pixel[3] + pixel_dst[1] * (255 - pixel[3]))) >> 8; + pixel_dst[2] = (uint32_t)((pixel[2] * pixel[3] + pixel_dst[2] * (255 - pixel[3]))) >> 8; + pixel_dst[3] = 255 - (255 - pixel[3]) * (255 - pixel_dst[3]); + } + } + } + } + if (img_alloc) + delete img_new; + } + return this; + } + + image::Image *Image::draw_rect(int x, int y, int w, int h, const image::Color &color, int thickness) + { + int ch_format = 0; + cv::Scalar cv_color; + _get_cv_format_color(_format, color, &ch_format, cv_color); + cv::Mat img(_height, _width, ch_format, _data); + cv::Rect rect(x, y, w, h); + if( color.alpha != 1 && (_format == Format::FMT_RGBA8888 || _format == Format::FMT_BGRA8888)) + { + cv::Mat roi = img(rect); + cv::Mat color_mat(roi.size(), roi.type(), cv_color); + cv::addWeighted(color_mat, color.alpha, roi, 1 - color.alpha, 0, roi); + } + else + cv::rectangle(img, rect, cv_color, thickness); + return this; + } + + image::Image *Image::draw_line(int x1, int y1, int x2, int y2, const image::Color &color, int thickness) + { + int ch_format = 0; + cv::Scalar cv_color; + _get_cv_format_color(_format, color, &ch_format, cv_color); + cv::Mat img(_height, _width, ch_format, _data); + cv::line(img, cv::Point(x1, y1), cv::Point(x2, y2), cv_color, thickness); + return this; + } + + image::Image *Image::draw_circle(int x, int y, int radius, const image::Color &color, int thickness) + { + int ch_format = 0; + cv::Scalar cv_color; + _get_cv_format_color(_format, color, &ch_format, cv_color); + cv::Mat img(_height, _width, ch_format, _data); + cv::circle(img, cv::Point(x, y), radius, cv_color, thickness); + return this; + } + + image::Image *Image::draw_ellipse(int x, int y, int a, int b, float angle, float start_angle, float end_angle, const image::Color &color, int thickness) + { + int ch_format = 0; + cv::Scalar cv_color; + _get_cv_format_color(_format, color, &ch_format, cv_color); + cv::Mat img(_height, _width, ch_format, _data); + cv::ellipse(img, cv::Point(x, y), cv::Size(a, b), angle, start_angle, end_angle, cv_color, thickness); + return this; + } + + image::Image *image::Image::draw_string(int x, int y, const std::string &text, const image::Color &color, float scale, int thickness, + bool wrap, int wrap_space, const std::string &font) + { + int ch_format = 0; + cv::Scalar cv_color; + add_default_fonts(fonts_info); + _get_cv_format_color(_format, color, &ch_format, cv_color); + cv::Mat img(_height, _width, ch_format, _data); + cv::Point point(x, y); + const std::string *final_font = &curr_font_name; + int final_font_id = curr_font_id; + // if cont is not empty, use it as font path + if (!font.empty()) + { + // if name in fonts_info + if (fonts_info.find(font) == fonts_info.end()) + { + log::error("font %d not load\n", font.c_str()); + throw std::runtime_error("font not load"); + } + final_font = &font; + final_font_id = get_default_fonts_id(font); + } + // auto wrap if text width > image width + if (!wrap) + { + _put_text(img, text, point, cv_color, scale, thickness, *final_font, final_font_id); + } + else + { + cv::Size size; + _get_text_size(size, text, *final_font, final_font_id, scale, thickness); + int text_width = size.width; + int text_height = size.height; + int text_max_width = _width - x; + bool wrap_now = false; + if(strstr(text.c_str(), "\n") != NULL || strstr(text.c_str(), "\r") != NULL) + wrap_now = true; + + // auto wrap + if (wrap_now || text_width > text_max_width) + { + // loop to calculate text width, if reach to image width, draw text on image, and move to next line go on + size_t idx = 0; + std::string text_tmp; + cv::Size size_tmp; + uint8_t char_size = 1; + bool last_is_r = false; + wrap_now = false; + while (idx < text.length()) + { + char c = text[idx]; + wrap_now = false; + if(c == '\r') + { + last_is_r = true; + wrap_now = true; + } + else if(c == '\n') + { + if(last_is_r) + { + last_is_r = false; + ++idx; + continue; + } + wrap_now = true; + last_is_r = false; + } + else + { + last_is_r = false; + } + if(wrap_now) + { + _put_text(img, text_tmp, point, cv_color, scale, thickness, *final_font, final_font_id); + point.x = x; + point.y += text_height + wrap_space; + text_tmp.clear(); + if (point.y + text_height >= _height) + break; + ++idx; + continue; + } + char_size = _get_char_size(c); + text_tmp += text.substr(idx, char_size); + cv::Size size_tmp; + _get_text_size(size_tmp, text_tmp, *final_font, final_font_id, scale, thickness); + if (size_tmp.width >= text_max_width) + { + if (size_tmp.width > text_max_width) + { + text_tmp.erase(text_tmp.length() - char_size, char_size); + } + _put_text(img, text_tmp, point, cv_color, scale, thickness, *final_font, final_font_id); + point.x = x; + point.y += text_height + wrap_space; + text_tmp.clear(); + if (point.y + text_height >= _height) + break; + if (size_tmp.width > text_max_width) + continue; + } + idx += char_size; + } + // draw last line + if (!text_tmp.empty()) + { + _put_text(img, text_tmp, point, cv_color, scale, thickness, *final_font, final_font_id); + } + } + else + { + _put_text(img, text, point, cv_color, scale, thickness, *final_font, final_font_id); + } + } + return this; + } + + image::Image *Image::draw_cross(int x, int y, const image::Color &color, int size, int thickness) + { + int ch_format = 0; + cv::Scalar cv_color; + _get_cv_format_color(_format, color, &ch_format, cv_color); + cv::Mat img(_height, _width, ch_format, _data); + cv::line(img, cv::Point(x - size, y), cv::Point(x + size, y), cv_color, thickness); + cv::line(img, cv::Point(x, y - size), cv::Point(x, y + size), cv_color, thickness); + return this; + } + + image::Image *Image::draw_arrow(int x0, int y0, int x1, int y1, const image::Color &color, int thickness) + { + int ch_format = 0; + cv::Scalar cv_color; + _get_cv_format_color(_format, color, &ch_format, cv_color); + cv::Mat img(_height, _width, ch_format, _data); + cv::Point start(x0, y0); + cv::Point end(x1, y1); + cv::arrowedLine(img, start, end, cv_color, thickness); + return this; + } + + image::Image *Image::draw_edges(std::vector> corners, const image::Color &color, int size, int thickness, bool fill) + { + int ch_format = 0; + cv::Scalar cv_color; + _get_cv_format_color(_format, color, &ch_format, cv_color); + cv::Mat img(_height, _width, ch_format, _data); + + if (corners.size() < 4) + { + throw std::runtime_error("corners size must >= 4"); + return nullptr; + } + + if (fill) { + thickness = -1; + } + + cv::Point topLeft(corners[0][0], corners[0][1]); + cv::Point bottomRight(corners[2][0], corners[2][1]); + cv::rectangle(img, topLeft, bottomRight, cv_color, thickness); + return this; + } + + image::Image *Image::draw_keypoints(std::vector keypoints, const image::Color &color, int size, int thickness) + { + int ch_format = 0; + cv::Scalar cv_color; + _get_cv_format_color(_format, color, &ch_format, cv_color); + cv::Mat img(_height, _width, ch_format, _data); + + if (keypoints.size() < 2 || keypoints.size() % 2 != 0) { + throw std::runtime_error("keypoints size must >= 2 and multiple of 2"); + return nullptr; + } + for(size_t i=0; idata()); + _cv_rgb_nv21(resize_rgb, dst, width, height); + } else { + dst = cv::Mat(height, width, pixel_num, ret->data()); + cv::resize(img, dst, cv::Size(width, height), 0, 0, inter_method); + } + } + else if (object_fit == image::Fit::FIT_CONTAIN) + { + cv::Mat tmp; + float scale = std::min((float)width / _width, (float)height / _height); + cv::resize(img, tmp, cv::Size(), scale, scale, inter_method); + dst = cv::Mat(cv_dst_h, width, pixel_num, ret->data()); + cv::Rect rect((width - tmp.cols) / 2, (height - tmp.rows) / 2, tmp.cols, tmp.rows); + tmp.copyTo(dst(rect)); + // fill black color + if (tmp.cols < width) + { + cv::Mat black(height, (width - tmp.cols) / 2, pixel_num, cv::Scalar(0, 0, 0)); + // left black area + cv::Rect rect(0, 0, (width - tmp.cols) / 2, height); + black.copyTo(dst(rect)); + // right black area + rect.x = (width - tmp.cols) / 2 + tmp.cols; + black.copyTo(dst(rect)); + if (rect.width != width - tmp.cols - rect.width) + { + // one line + rect.x = dst.cols - 1; + rect.width = 1; + cv::Mat black2(height, 1, pixel_num, cv::Scalar(0, 0, 0)); + black2.copyTo(dst(rect)); + } + } + if (tmp.rows < height) + { + cv::Mat black((height - tmp.rows) / 2, width, pixel_num, cv::Scalar(0, 0, 0)); + // top black area + cv::Rect rect(0, 0, width, (height - tmp.rows) / 2); + black.copyTo(dst(rect)); + // bottom black area + rect.y = (height - tmp.rows) / 2 + tmp.rows; + black.copyTo(dst(rect)); + if (rect.height != height - tmp.rows - rect.height) + { + // one line + rect.y = dst.rows - 1; + rect.height = 1; + cv::Mat black2(1, width, pixel_num, cv::Scalar(0, 0, 0)); + black2.copyTo(dst(rect)); + } + } + } + else if (object_fit == image::Fit::FIT_COVER) + { + cv::Mat tmp; + float scale = std::max((float)width / _width, (float)height / _height); + cv::resize(img, tmp, cv::Size(), scale, scale, inter_method); + dst = cv::Mat(cv_dst_h, width, pixel_num, ret->data()); + cv::Rect rect((tmp.cols - width) / 2, (tmp.rows - height) / 2, width, height); + tmp(rect).copyTo(dst); + } + else + { + throw std::runtime_error("not support object fit"); + } + return ret; + } + + image::Image *Image::affine(std::vector src_points, std::vector dst_points, int width, int height, image::ResizeMethod method) + { + if (width < 0 && height < 0) + { + throw std::runtime_error("width and height can't both be -1"); + } + int pixel_num = _get_cv_pixel_num(_format); + /// calculate size if width or height is -1 + if (width == -1) + { + width = height * _width / _height; + } + else if (height == -1) + { + height = width * _height / _width; + } + image::Image *ret = new image::Image(width, height, _format); + ; + cv::Mat img(_height, _width, pixel_num, _data); + cv::Mat dst(height, width, pixel_num, ret->data()); + cv::Point2f srcTri[3]; + cv::Point2f dstTri[3]; + for (int i = 0; i < 3; i++) + { + srcTri[i] = cv::Point2f(src_points[i * 2], src_points[i * 2 + 1]); + dstTri[i] = cv::Point2f(dst_points[i * 2], dst_points[i * 2 + 1]); + } + cv::Mat warp_mat = cv::getAffineTransform(srcTri, dstTri); + cv::warpAffine(img, dst, warp_mat, dst.size(), (cv::InterpolationFlags)method); + return ret; + } + + image::Image *Image::copy() + { + image::Image *ret = new image::Image(_width, _height, _format); + ; + memcpy(ret->data(), _data, _width * _height * image::fmt_size[_format]); + return ret; + } + + image::Image *Image::crop(int x, int y, int w, int h) + { + image::Image *ret = new image::Image(w, h, _format); + ; + int pixel_num = _get_cv_pixel_num(_format); + cv::Mat img(_height, _width, pixel_num, _data); + cv::Mat dst(h, w, pixel_num, ret->data()); + cv::Rect rect(x, y, w, h); + img(rect).copyTo(dst); + return ret; + } + + image::Image *Image::rotate(float angle, int width, int height, image::ResizeMethod method) + { + int pixel_num = _get_cv_pixel_num(_format); + if (width < 0 && height < 0) + { + if (angle == 90 || angle == 270) + { + width = _height; + height = _width; + } + } + /// calculate size if width or height is -1 + if (width < 0) + { + // new_H = int(w * fabs(sin(radians(angle))) + h * fabs(cos(radians(angle)))) + width = _width * fabs(sin(angle * M_PI / 180)) + _height * fabs(cos(angle * M_PI / 180)); + } + if (height < 0) + { + // new_W = int(h * fabs(sin(radians(angle))) + w * fabs(cos(radians(angle)))) + height = _height * fabs(sin(angle * M_PI / 180)) + _width * fabs(cos(angle * M_PI / 180)); + } + image::Image *ret = new image::Image(width, height, _format); + ; + cv::Mat img(_height, _width, pixel_num, _data); + cv::Mat dst(height, width, pixel_num, ret->data()); + cv::Point2f center((float)_width / 2.0, (float)_height / 2.0); + cv::Mat rot_mat = cv::getRotationMatrix2D(center, angle, 1.0); + // M[0, 2] += (width - w) / 2 + // M[1, 2] += (height - h) / 2 + rot_mat.at(0, 2) += (width - _width) / 2.0; + rot_mat.at(1, 2) += (height - _height) / 2.0; + cv::warpAffine(img, dst, rot_mat, dst.size(), (cv::InterpolationFlags)method); + return ret; + } + + err::Err Image::save(const char *path, int quality) + { + if (_height <= 0 || _width <= 0) + { + log::error("save image failed, image size is invalid\n"); + return err::ERR_ARGS; + } + cv::Mat img(_height, _width, CV_8UC((int)image::fmt_size[_format]), _data); + std::vector params; + if (quality >= 0 && quality <= 100) + { + params.push_back(cv::IMWRITE_JPEG_QUALITY); + params.push_back(quality); + } + else if (quality >= -1 && quality <= 9) + { + params.push_back(cv::IMWRITE_PNG_COMPRESSION); + params.push_back(quality); + } + else + { + return err::ERR_ARGS; + } + log::debug("save image to %s\n", path); + // not not dir exists, create it + std::string dir = path; + size_t pos = dir.find_last_of('/'); + if (pos != std::string::npos) + { + dir = dir.substr(0, pos); + if (!fs::exists(dir)) + { + if (fs::mkdir(dir) < 0) + { + log::error("create dir %s failed\n", dir.c_str()); + return err::ERR_IO; + } + } + } + // save, img may RGB format + if (_format == image::FMT_RGB888) + { + cv::cvtColor(img, img, cv::COLOR_RGB2BGR); + } + else if (_format == image::FMT_RGBA8888) + { + cv::cvtColor(img, img, cv::COLOR_RGBA2BGRA); + } + cv::imwrite(path, img, params); + // convert back + if (_format == image::FMT_RGB888) + { + cv::cvtColor(img, img, cv::COLOR_RGB2BGR); + } + else if (_format == image::FMT_RGBA8888) + { + cv::cvtColor(img, img, cv::COLOR_RGBA2BGRA); + } + return err::ERR_NONE; + } + + image::Size string_size(std::string text, float scale, int thickness, const std::string &font) + { + const std::string *final_font = &curr_font_name; + int final_font_id = curr_font_id; + // if cont is not empty, use it as font path + if (!font.empty()) + { + // if name in fonts_info + if (fonts_info.find(font) == fonts_info.end()) + { + log::error("font %d not load\n", font.c_str()); + throw std::runtime_error("font not load"); + } + final_font = &font; + final_font_id = get_default_fonts_id(font); + } + cv::Size size; + _get_text_size(size, text, *final_font, final_font_id, scale, thickness); + return Size(size.width, size.height); + } + + std::vector resize_map_pos(int w_in, int h_in, int w_out, int h_out, image::Fit fit, int x, int y, int w, int h) { + float scale_x = static_cast(w_out) / w_in; + float scale_y = static_cast(h_out) / h_in; + std::vector result; + + if (fit == image::Fit::FIT_FILL) { + x = static_cast(x * scale_x); + y = static_cast(y * scale_y); + if (w > 0) { + w = static_cast(w * scale_x); + } + if (h > 0) { + h = static_cast(h * scale_y); + } + } else if (fit == image::Fit::FIT_CONTAIN) { + float scale = std::min(scale_x, scale_y); + if (w_out > h_out) { + x = static_cast((w_out - w_in * scale) / 2 + x * scale); + y = static_cast(y * scale); + } else { + x = static_cast(x * scale); + y = static_cast((h_out - h_in * scale) / 2 + y * scale); + } + if (w > 0) { + w = static_cast(w * scale); + h = static_cast(h * scale); + } + } else if (fit == image::Fit::FIT_COVER) { + float scale = std::max(scale_x, scale_y); + if (w_out > h_out) { + x = static_cast(x * scale); + y = static_cast((h_out - h_in * scale) / 2 + y * scale); + } else { + x = static_cast((w_out - w_in * scale) / 2 + x * scale); + y = static_cast(y * scale); + } + if (w > 0) { + w = static_cast(w * scale); + h = static_cast(h * scale); + } + } else { + throw err::Exception(err::ERR_ARGS, "Unsupported fit mode"); + } + + // Populate the result vector + result.push_back(x); + result.push_back(y); + if (w > 0 && h > 0) { + result.push_back(w); + result.push_back(h); + } + return result; + } + + std::vector resize_map_pos_reverse(int w_in, int h_in, int w_out, int h_out, image::Fit fit, int x, int y, int w, int h) { + float scale_x = static_cast(w_out) / w_in; + float scale_y = static_cast(h_out) / h_in; + std::vector result; + + if (fit == image::Fit::FIT_FILL) { + x = static_cast(x / scale_x); + y = static_cast(y / scale_y); + if (w > 0) { + w = static_cast(w / scale_x); + } + if (h > 0) { + h = static_cast(h / scale_y); + } + } else if (fit == image::Fit::FIT_CONTAIN) { + float scale = std::min(scale_x, scale_y); + if (w_out > h_out) { + x = static_cast((x - (w_out - w_in * scale) / 2) / scale); + y = static_cast(y / scale); + } else { + x = static_cast(x / scale); + y = static_cast((y - (h_out - h_in * scale) / 2) / scale); + } + if (w > 0) { + w = static_cast(w / scale); + h = static_cast(h / scale); + } + } else if (fit == image::Fit::FIT_COVER) { + float scale = std::max(scale_x, scale_y); + if (w_out > h_out) { + x = static_cast(x / scale); + y = static_cast((y - (h_out - h_in * scale) / 2) / scale); + } else { + x = static_cast((x - (w_out - w_in * scale) / 2) / scale); + y = static_cast(y * scale); + } + if (w > 0) { + w = static_cast(w / scale); + h = static_cast(h / scale); + } + } else { + throw err::Exception(err::ERR_ARGS, "Unsupported fit mode"); + } + + // Populate the result vector + result.push_back(x); + result.push_back(y); + if (w > 0 && h > 0) { + result.push_back(w); + result.push_back(h); + } + return result; + } + + void _get_cv_format_color(image::Format _format, const image::Color &color_in, int *ch_format, cv::Scalar &cv_color) + { + bool color_alloc = false; + image::Color *color = (image::Color *)&color_in; + if (color->format != _format) + { + color = color->to_format2(_format); + color_alloc = true; + } + switch (_format) + { + case image::FMT_RGB888: + *ch_format = CV_8UC3; + cv_color = cv::Scalar(color->r, color->g, color->b); + break; + case image::FMT_BGR888: + *ch_format = CV_8UC3; + cv_color = cv::Scalar(color->b, color->g, color->r); + break; + case image::FMT_GRAYSCALE: + *ch_format = CV_8UC1; + cv_color = cv::Scalar(color->gray); + break; + case image::FMT_BGRA8888: + *ch_format = CV_8UC4; + cv_color = cv::Scalar(color->b, color->g, color->r, color->alpha * 255); + break; + case image::FMT_RGBA8888: + *ch_format = CV_8UC4; + cv_color = cv::Scalar(color->r, color->g, color->b, color->alpha * 255); + break; + default: + throw std::runtime_error("not support format"); + break; + } + if (color_alloc) + delete color; + } + + int Image::_get_cv_pixel_num(image::Format &format) + { + int pixel_num = 0; + switch (_format) + { + case image::FMT_RGB888: + case image::FMT_BGR888: + pixel_num = CV_8UC3; + break; + case image::FMT_GRAYSCALE: + pixel_num = CV_8UC1; + break; + case image::FMT_BGRA8888: + case image::FMT_RGBA8888: + pixel_num = CV_8UC4; + break; + case image::FMT_RGB565: + case image::FMT_BGR565: + pixel_num = CV_8UC2; + break; + default: + throw std::runtime_error("not support format"); + break; + } + return pixel_num; + } + + std::vector Image::_get_available_roi(std::vector roi, std::vector other_roi) { + int src_x, src_y, src_w, src_h; + + if (other_roi.size() == 0) { + src_x = 0; + src_y = 0; + src_w = _width; + src_h = _height; + } else if (other_roi.size() == 4) { + src_x = other_roi[0]; + src_y = other_roi[1]; + src_w = other_roi[2]; + src_h = other_roi[3]; + } else { + throw err::Exception("other_roi size must be 4"); + } + + if (roi.size() == 0) { + return std::vector{0, 0, _width, _height}; + } else if (roi.size() != 4) { + throw err::Exception("roi size must be 4"); + } + + if (roi[2] <= 0 || roi[3] <= 0) { + throw err::Exception("roi width and height must > 0"); + } + + if ((roi[0] >= (src_x + src_w)) || (roi[1] >= (src_y + src_h)) || (roi[0] + roi[2] <= src_x) || (roi[1] + roi[3] <= src_y)) { + throw err::Exception("roi does not overlap on the image!"); + } + + int x = std::max(roi[0], src_x); + int y = std::max(roi[1], src_y); + int w = std::min(roi[0] + roi[2], src_x + src_w) - x; + int h = std::min(roi[1] + roi[3], src_y + src_h) - y; + return std::vector{x, y, w, h}; + } +} // namespace maix::image diff --git a/vision/components/vision/src/maix_image_ops.cpp b/vision/components/vision/src/maix_image_ops.cpp new file mode 100644 index 0000000..dd3eb17 --- /dev/null +++ b/vision/components/vision/src/maix_image_ops.cpp @@ -0,0 +1,1209 @@ +/** + * @author lxowalle@sipeed + * @copyright Sipeed Ltd 2023- + * @license Apache 2.0 + * @update 2023.9.8: Add framework, create this file. + */ + +#include "maix_image.hpp" +#include "maix_image_util.hpp" +#include "maix_err.hpp" +#include +#include + +namespace maix::image { + void convert_to_imlib_image(image::Image *image, image_t *imlib_image) { + if (!image || !imlib_image) { + return; + } + + pixformat_t imlib_format; + switch (image->format()) { + case Format::FMT_GRAYSCALE: + imlib_format = PIXFORMAT_GRAYSCALE; + break; + case Format::FMT_RGB565: + imlib_format = PIXFORMAT_RGB565; + break; + case Format::FMT_BGR888: // fall through + case Format::FMT_RGB888: + imlib_format = PIXFORMAT_RGB888; + break; + default: + log::error("convert_to_imlib_image format not support: %d", image->format()); + return; + } + + image_init(imlib_image, image->width(), image->height(), imlib_format, image->data_size(), image->data()); + } + + image::Image *Image::mean_pool(int x_div, int y_div, bool copy) { + err::check_bool_raise(x_div > 0 && x_div <= _width && y_div > 0 && y_div <= _height, "mean pool get invalid param"); + + image_t src_img, out_img; + uint8_t *buffer = NULL; + convert_to_imlib_image(this, &src_img); + + out_img.w = src_img.w / x_div; + out_img.h = src_img.h / y_div; + out_img.pixfmt = src_img.pixfmt; + if (copy) { + buffer = (uint8_t *)malloc(out_img.w * out_img.h * image::fmt_size[_format]); + if (!buffer) { + log::error("mean pool malloc failed\r\n"); + return nullptr; + } + out_img.pixels = buffer; + } else { + out_img.pixels = src_img.pixels; + } + + imlib_mean_pool(&src_img, &out_img, x_div, y_div); + + if (copy) { + image::Image *dst = new image::Image(out_img.w, out_img.h, _format, out_img.pixels, -1, true); + return dst; + } else { + _width = out_img.w; + _height = out_img.h; + } + return this; + } + + image::Image *Image::midpoint_pool(int x_div, int y_div, double bias, bool copy) { + if (x_div <= 0 || x_div > _width || y_div <= 0 || y_div > _height) { + log::warn("midpoint pool invalid div: %d, %d", x_div, y_div); + return nullptr; + } + + if (!(_format == Format::FMT_RGB888 || _format == Format::FMT_BGR888 || _format == Format::FMT_GRAYSCALE)) { + log::warn("midpoint pool not support format: %d", _format); + return nullptr; + } + + image::Image *dst; + int _dst_width = _width / x_div; + int _dst_height = _height / y_div; + if (copy) { + dst = new image::Image(_dst_width, _dst_height, _format); + } else { + dst = this; + } + + bias *= 256; + int min_bias = 256 - bias; + int max_bias = bias; + + switch (_format) { + case Format::FMT_GRAYSCALE: + for (int y = 0, yy = _dst_height, yyy = (_height % y_div) / 2; y < yy; y++) { + for (int x = 0, xx = _dst_width, xxx = (_width % x_div) / 2; x < xx; x++) { + int min = 255, max = 0; + uint8_t *src_data = (uint8_t *)_data; + uint8_t *dst_data = (uint8_t *)dst->data(); + for (int i = 0; i < y_div; i++) { + for (int j = 0; j < x_div; j++) { + int pixel = src_data[(yyy + (y * y_div) + i) * _width + (xxx + (x * x_div) + j)]; + min = std::min(min, pixel); + max = std::max(max, pixel); + } + } + dst_data[y * _dst_width + x] = (min_bias * min + max_bias * max) >> 8; + } + } + break; + case Format::FMT_RGB888: // fall through + case Format::FMT_BGR888: + for (int y = 0, yy = _dst_height, yyy = (_height % y_div) / 2; y < yy; y++) { + for (int x = 0, xx = _dst_width, xxx = (_width % x_div) / 2; x < xx; x++) { + int v0_min = 255, v0_max = 0; + int v1_min = 255, v1_max = 0; + int v2_min = 255, v2_max = 0; + uint8_t *src_data = (uint8_t *)_data; + uint8_t *dst_data = (uint8_t *)dst->data(); + for (int i = 0; i < y_div; i++) { + for (int j = 0; j < x_div; j++) { + int v0 = src_data[((yyy + (y * y_div) + i) * _width + (xxx + (x * x_div) + j)) * 3]; + int v1 = src_data[((yyy + (y * y_div) + i) * _width + (xxx + (x * x_div) + j)) * 3 + 1]; + int v2 = src_data[((yyy + (y * y_div) + i) * _width + (xxx + (x * x_div) + j)) * 3 + 2]; + v0_min = std::min(v0_min, v0); + v0_max = std::max(v0_max, v0); + v1_min = std::min(v1_min, v1); + v1_max = std::max(v1_max, v1); + v2_min = std::min(v2_min, v2); + v2_max = std::max(v2_max, v2); + } + } + + dst_data[(y * _dst_width + x) * 3] = ((v0_min * min_bias) + (v0_max * max_bias)) >> 8; + dst_data[(y * _dst_width + x) * 3 + 1] =((v1_min * min_bias) + (v1_max * max_bias)) >> 8; + dst_data[(y * _dst_width + x) * 3 + 2] = ((v2_min * min_bias) + (v2_max * max_bias)) >> 8; + } + } + break; + default: + // should not be here + break; + } + + if (!copy) { + _width = _dst_width; + _height = _dst_height; + } + return dst; + } + + image::Image *Image::compress(int quality) { + return to_jpeg(quality); + } + + err::Err image_zero(image::Image &src, image::Image &mask, bool invert) { + image_t src_img, mask_img; + convert_to_imlib_image(&src, &src_img); + convert_to_imlib_image(&mask, &mask_img); + imlib_zero(&src_img, &mask_img, invert); + + return err::Err::ERR_NONE; + } + + image::Image *Image::clear(image::Image *mask) { + if (!mask) { + memset(_data, 0, _data_size); + } else { + image_zero(*this, *mask, false); + } + + return this; + } + + image::Image *Image::mask_rectange(int x, int y, int w, int h) { + int use_default_setting = 0; + if (x < 0 || y < 0 || w < 0 || h < 0) { + use_default_setting = 1; + } + + if (use_default_setting) { + x = _width / 4; + y = _height / 4; + w = _width / 2; + h = _height / 2; + } + + draw_rect(x, y, w, h, image::COLOR_BLACK, -1); + return this; + } + + image::Image *Image::mask_circle(int x, int y, int radius) { + int use_default_setting = 0; + if (x < 0 || y < 0 || radius < 0) { + use_default_setting = 1; + } + + if (use_default_setting) { + x = _width / 2; + y = _height / 2; + radius = std::min(_width, _height) / 2; + } + + draw_circle(x, y, radius, image::COLOR_BLACK, -1); + return this; + } + + image::Image *Image::mask_ellipse(int x, int y, int radius_x, int radius_y, float rotation_angle_in_degrees) { + int use_default_setting = 0; + if (x < 0 || y < 0 || radius_x < 0 || radius_y < 0) { + use_default_setting = 1; + } + + if (use_default_setting) { + x = _width / 2; + y = _height / 2; + radius_x = _width / 2; + radius_y = _height / 2; + rotation_angle_in_degrees = 0; + } + + draw_ellipse(x, y, radius_x, radius_y, rotation_angle_in_degrees, 0, 360, image::COLOR_BLACK, -1); + return this; + } + + image::Image *Image::binary(std::vector> thresholds, bool invert, bool zero, image::Image *mask, bool to_bitmap, bool copy) { + err::check_bool_raise(thresholds.size() != 0, "You need to set thresholds"); + err::check_bool_raise(to_bitmap == false, "Parameter to_bitmap is not supported"); + + list_t thresholds_list; + list_init(&thresholds_list, sizeof(color_thresholds_list_lnk_data_t)); + _convert_to_lab_thresholds(thresholds, &thresholds_list); + + image_t src_img, mask_img, out_img; + image::Image *dst = nullptr; + if (copy) { + dst = new image::Image(_width, _height, _format); + } else { + dst = this; + } + + convert_to_imlib_image(this, &src_img); + convert_to_imlib_image(dst, &out_img); + if (mask) { + convert_to_imlib_image(mask, &mask_img); + } + + imlib_binary(&out_img, &src_img, &thresholds_list, invert, zero, mask ? &mask_img : NULL); + list_free(&thresholds_list); + + return dst; + } + + image::Image *Image::invert() { + int remain_len = _data_size % 4; + int u32_len = (_data_size - remain_len) >> 2; + uint8_t *remain_data = (uint8_t *)((uint8_t *)_data + (u32_len << 2)); + uint32_t *u32_data = (uint32_t *)_data; + for (int i = 0; i < u32_len; i ++) { + u32_data[i] = ~u32_data[i]; + } + + for (int i = 0; i < remain_len; i ++) { + remain_data[i] = ~remain_data[i]; + } + + return this; + } + + image::Image *Image::b_and(image::Image *other, image::Image *mask) { + image_t src_img, other_img, mask_img; + + err::check_bool_raise(other != NULL && other->data() != NULL, "Other image is null"); + err::check_bool_raise(_format == other->format(), "Other image format is not match source image"); + err::check_bool_raise(_width == other->width() && _height == other->height(), "Other image size is not match source image"); + + if (mask) { + convert_to_imlib_image(this, &src_img); + convert_to_imlib_image(other, &other_img); + err::check_bool_raise(_width == mask->width() && _height == mask->height(), "Mask image size is not match source image"); + convert_to_imlib_image(mask, &mask_img); + imlib_b_and(&src_img, NULL, other ? &other_img : NULL, 0, mask ? &mask_img : NULL); + } else { + int remain_len = _data_size % 4; + int u32_len = (_data_size - remain_len) >> 2; + uint8_t *remain_src_data = (uint8_t *)((uint8_t *)_data + (u32_len << 2)); + uint32_t *u32_src_data = (uint32_t *)_data; + uint8_t *remain_other_data = (uint8_t *)((uint8_t *)other->data() + (u32_len << 2)); + uint32_t *u32_other_data = (uint32_t *)other->data(); + for (int i = 0; i < u32_len; i ++) { + u32_src_data[i] = u32_src_data[i] & u32_other_data[i]; + } + + for (int i = 0; i < remain_len; i ++) { + remain_src_data[i] = remain_src_data[i] & remain_other_data[i]; + } + } + + return this; + } + + image::Image *Image::b_nand(image::Image *other, image::Image *mask) { + image_t src_img, other_img, mask_img; + + err::check_bool_raise(other != NULL && other->data() != NULL, "Other image is null"); + err::check_bool_raise(_format == other->format(), "Other image format is not match source image"); + err::check_bool_raise(_width == other->width() && _height == other->height(), "Other image size is not match source image"); + + if (mask) { + convert_to_imlib_image(this, &src_img); + convert_to_imlib_image(other, &other_img); + err::check_bool_raise(_width == mask->width() && _height == mask->height(), "Mask image size is not match source image"); + convert_to_imlib_image(mask, &mask_img); + imlib_b_nand(&src_img, NULL, other ? &other_img : NULL, 0, mask ? &mask_img : NULL); + } else { + int remain_len = _data_size % 4; + int u32_len = (_data_size - remain_len) >> 2; + uint8_t *remain_src_data = (uint8_t *)((uint8_t *)_data + (u32_len << 2)); + uint32_t *u32_src_data = (uint32_t *)_data; + uint8_t *remain_other_data = (uint8_t *)((uint8_t *)other->data() + (u32_len << 2)); + uint32_t *u32_other_data = (uint32_t *)other->data(); + for (int i = 0; i < u32_len; i ++) { + u32_src_data[i] = u32_src_data[i] & ~u32_other_data[i]; + } + + for (int i = 0; i < remain_len; i ++) { + remain_src_data[i] = remain_src_data[i] & ~remain_other_data[i]; + } + } + + return this; + } + + image::Image *Image::b_or(image::Image *other, image::Image *mask) { + image_t src_img, other_img, mask_img; + + err::check_bool_raise(other != NULL && other->data() != NULL, "Other image is null"); + err::check_bool_raise(_format == other->format(), "Other image format is not match source image"); + err::check_bool_raise(_width == other->width() && _height == other->height(), "Other image size is not match source image"); + + if (mask) { + convert_to_imlib_image(this, &src_img); + convert_to_imlib_image(other, &other_img); + err::check_bool_raise(_width == mask->width() && _height == mask->height(), "Mask image size is not match source image"); + convert_to_imlib_image(mask, &mask_img); + imlib_b_or(&src_img, NULL, other ? &other_img : NULL, 0, mask ? &mask_img : NULL); + } else { + int remain_len = _data_size % 4; + int u32_len = (_data_size - remain_len) >> 2; + uint8_t *remain_src_data = (uint8_t *)((uint8_t *)_data + (u32_len << 2)); + uint32_t *u32_src_data = (uint32_t *)_data; + uint8_t *remain_other_data = (uint8_t *)((uint8_t *)other->data() + (u32_len << 2)); + uint32_t *u32_other_data = (uint32_t *)other->data(); + for (int i = 0; i < u32_len; i ++) { + u32_src_data[i] = u32_src_data[i] | u32_other_data[i]; + } + + for (int i = 0; i < remain_len; i ++) { + remain_src_data[i] = remain_src_data[i] | remain_other_data[i]; + } + } + + + return this; + } + + image::Image *Image::b_nor(image::Image *other, image::Image *mask) { + image_t src_img, other_img, mask_img; + + err::check_bool_raise(other != NULL && other->data() != NULL, "Other image is null"); + err::check_bool_raise(_format == other->format(), "Other image format is not match source image"); + err::check_bool_raise(_width == other->width() && _height == other->height(), "Other image size is not match source image"); + + if (mask) { + convert_to_imlib_image(this, &src_img); + convert_to_imlib_image(other, &other_img); + err::check_bool_raise(_width == mask->width() && _height == mask->height(), "Mask image size is not match source image"); + convert_to_imlib_image(mask, &mask_img); + imlib_b_nor(&src_img, NULL, other ? &other_img : NULL, 0, mask ? &mask_img : NULL); + } else { + int remain_len = _data_size % 4; + int u32_len = (_data_size - remain_len) >> 2; + uint8_t *remain_src_data = (uint8_t *)((uint8_t *)_data + (u32_len << 2)); + uint32_t *u32_src_data = (uint32_t *)_data; + uint8_t *remain_other_data = (uint8_t *)((uint8_t *)other->data() + (u32_len << 2)); + uint32_t *u32_other_data = (uint32_t *)other->data(); + for (int i = 0; i < u32_len; i ++) { + u32_src_data[i] = u32_src_data[i] | ~u32_other_data[i]; + } + + for (int i = 0; i < remain_len; i ++) { + remain_src_data[i] = remain_src_data[i] | ~remain_other_data[i]; + } + } + + + return this; + } + + image::Image *Image::b_xor(image::Image *other, image::Image *mask) { + image_t src_img, other_img, mask_img; + + err::check_bool_raise(other != NULL && other->data() != NULL, "Other image is null"); + err::check_bool_raise(_format == other->format(), "Other image format is not match source image"); + err::check_bool_raise(_width == other->width() && _height == other->height(), "Other image size is not match source image"); + + if (mask) { + convert_to_imlib_image(this, &src_img); + convert_to_imlib_image(other, &other_img); + err::check_bool_raise(_width == mask->width() && _height == mask->height(), "Mask image size is not match source image"); + convert_to_imlib_image(mask, &mask_img); + imlib_b_xor(&src_img, NULL, other ? &other_img : NULL, 0, mask ? &mask_img : NULL); + } else { + int remain_len = _data_size % 4; + int u32_len = (_data_size - remain_len) >> 2; + uint8_t *remain_src_data = (uint8_t *)((uint8_t *)_data + (u32_len << 2)); + uint32_t *u32_src_data = (uint32_t *)_data; + uint8_t *remain_other_data = (uint8_t *)((uint8_t *)other->data() + (u32_len << 2)); + uint32_t *u32_other_data = (uint32_t *)other->data(); + for (int i = 0; i < u32_len; i ++) { + u32_src_data[i] = u32_src_data[i] ^ u32_other_data[i]; + } + + for (int i = 0; i < remain_len; i ++) { + remain_src_data[i] = remain_src_data[i] ^ remain_other_data[i]; + } + } + + return this; + } + + image::Image *Image::b_xnor(image::Image *other, image::Image *mask) { + image_t src_img, other_img, mask_img; + + err::check_bool_raise(other != NULL && other->data() != NULL, "Other image is null"); + err::check_bool_raise(_format == other->format(), "Other image format is not match source image"); + err::check_bool_raise(_width == other->width() && _height == other->height(), "Other image size is not match source image"); + + if (mask) { + convert_to_imlib_image(this, &src_img); + convert_to_imlib_image(other, &other_img); + err::check_bool_raise(_width == mask->width() && _height == mask->height(), "Mask image size is not match source image"); + convert_to_imlib_image(mask, &mask_img); + imlib_b_xnor(&src_img, NULL, other ? &other_img : NULL, 0, mask ? &mask_img : NULL); + } else { + int remain_len = _data_size % 4; + int u32_len = (_data_size - remain_len) >> 2; + uint8_t *remain_src_data = (uint8_t *)((uint8_t *)_data + (u32_len << 2)); + uint32_t *u32_src_data = (uint32_t *)_data; + uint8_t *remain_other_data = (uint8_t *)((uint8_t *)other->data() + (u32_len << 2)); + uint32_t *u32_other_data = (uint32_t *)other->data(); + for (int i = 0; i < u32_len; i ++) { + u32_src_data[i] = u32_src_data[i] ^ ~u32_other_data[i]; + } + + for (int i = 0; i < remain_len; i ++) { + remain_src_data[i] = remain_src_data[i] ^ ~remain_other_data[i]; + } + } + + return this; + } + + image::Image *Image::awb(bool max) { + image_t src_img; + Image *rgb565_img = nullptr; + if (_format == image::FMT_RGB888 || _format == image::FMT_BGR888) { + rgb565_img = to_format(image::FMT_RGB565); + convert_to_imlib_image(rgb565_img, &src_img); + } else { + log::warn("awb not support format: %d", _format); + return this; + } + + imlib_awb(&src_img, max); + + if (_format == image::FMT_RGB888 || _format == image::FMT_BGR888) { + Image *rgb888_img = rgb565_img->to_format(image::FMT_RGB888); + memcpy(_data, rgb888_img->data(), _data_size); + delete rgb888_img; + delete rgb565_img; + } + return this; + } + + image::Image *Image::ccm(std::vector &matrix) { + image_t src_img; + convert_to_imlib_image(this, &src_img); + + float ccm[12] = {}; + float *matrix_data = (float *)matrix.data(); + size_t len = matrix.size(); + if (len != 9 && len != 12) { + log::error("ccm matrix size not match: %d", len); + return this; + } + + for (size_t i = 0; i < len; i ++) { + ccm[i] = matrix_data[i]; + } + + imlib_ccm(&src_img, ccm, len == 12); + return this; + } + + image::Image *Image::gamma(double gamma, double contrast, double brightness) { + image_t src_img; + convert_to_imlib_image(this, &src_img); + + imlib_gamma(&src_img, gamma, contrast, brightness); + return this; + } + + image::Image *Image::gamma_corr(double gamma, double contrast, double brightness) { + return this->gamma(gamma, contrast, brightness); + } + + image::Image *Image::negate(void) { + image_t src_img; + convert_to_imlib_image(this, &src_img); + imlib_negate(&src_img); + return this; + } + + image::Image *Image::replace(image::Image *other, bool hmirror, bool vflip, bool transpose, image::Image *mask) { + image_t src_img, other_img, mask_img; + convert_to_imlib_image(this, &src_img); + + if (other) { + err::check_bool_raise(_format == other->format(), "Other image format is not match source image"); + err::check_bool_raise(_width == other->width() && _height == other->height(), "Other image size is not match source image"); + convert_to_imlib_image(other, &other_img); + } + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + } + + if (other && mask) { + imlib_replace(&src_img, NULL, &other_img, 0, hmirror, vflip, transpose, &mask_img); + } else if (other && !mask) { + imlib_replace(&src_img, NULL, &other_img, 0, hmirror, vflip, transpose, NULL); + } else if (!other && mask) { + imlib_replace(&src_img, NULL, &src_img, 0, hmirror, vflip, transpose, &mask_img); + } else { + imlib_replace(&src_img, NULL, &src_img, 0, hmirror, vflip, transpose, NULL); + } + + _width = src_img.w; + _height = src_img.h; + + return this; + } + + image::Image *Image::set(image::Image *other, bool hmirror, bool vflip, bool transpose, image::Image *mask) { + return this->replace(other, hmirror, vflip, transpose, mask); + } + + image::Image *Image::add(image::Image *other, image::Image *mask) { + image_t src_img, other_img, mask_img; + convert_to_imlib_image(this, &src_img); + convert_to_imlib_image(other, &other_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_add(&src_img, NULL, &other_img, 0, &mask_img); + } else { + imlib_add(&src_img, NULL, &other_img, 0, NULL); + } + return this; + } + + image::Image *Image::sub(image::Image *other, bool reverse, image::Image *mask) { + image_t src_img, other_img, mask_img; + convert_to_imlib_image(this, &src_img); + convert_to_imlib_image(other, &other_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_sub(&src_img, NULL, &other_img, 0, reverse, &mask_img); + } else { + imlib_sub(&src_img, NULL, &other_img, 0, reverse, NULL); + } + return this; + } + + image::Image *Image::mul(image::Image *other, bool invert, image::Image *mask) { + image_t src_img, other_img, mask_img; + convert_to_imlib_image(this, &src_img); + convert_to_imlib_image(other, &other_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_mul(&src_img, NULL, &other_img, 0, invert, &mask_img); + } else { + imlib_mul(&src_img, NULL, &other_img, 0, invert, NULL); + } + return this; + } + + image::Image *Image::div(image::Image *other, bool invert, bool mod, image::Image *mask) { + image_t src_img, other_img, mask_img; + convert_to_imlib_image(this, &src_img); + convert_to_imlib_image(other, &other_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_div(&src_img, NULL, &other_img, 0, invert, mod, &mask_img); + } else { + imlib_div(&src_img, NULL, &other_img, 0, invert, mod, NULL); + } + return this; + } + + image::Image *Image::min(image::Image *other, image::Image *mask) { + image_t src_img, other_img, mask_img; + convert_to_imlib_image(this, &src_img); + convert_to_imlib_image(other, &other_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_min(&src_img, NULL, &other_img, 0, &mask_img); + } else { + imlib_min(&src_img, NULL, &other_img, 0, NULL); + } + return this; + } + + image::Image *Image::max(image::Image *other, image::Image *mask) { + image_t src_img, other_img, mask_img; + convert_to_imlib_image(this, &src_img); + convert_to_imlib_image(other, &other_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_max(&src_img, NULL, &other_img, 0, &mask_img); + } else { + imlib_max(&src_img, NULL, &other_img, 0, NULL); + } + return this; + } + + image::Image *Image::difference(image::Image *other, image::Image *mask) { + image_t src_img, other_img, mask_img; + convert_to_imlib_image(this, &src_img); + convert_to_imlib_image(other, &other_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_difference(&src_img, NULL, &other_img, 0, &mask_img); + } else { + imlib_difference(&src_img, NULL, &other_img, 0, NULL); + } + return this; + } + + image::Image *Image::blend(image::Image *other, int alpha, image::Image *mask) { + image_t src_img, other_img, mask_img; + convert_to_imlib_image(this, &src_img); + convert_to_imlib_image(other, &other_img); + + if (alpha < 0 || alpha > 256) { + log::error("alpha value not valid: %d", alpha); + return this; + } + + float alpha_f = alpha / 256.0; + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_blend(&src_img, NULL, &other_img, 0, alpha_f, &mask_img); + } else { + imlib_blend(&src_img, NULL, &other_img, 0, alpha_f, NULL); + } + return this; + } + + image::Image *Image::histeq(bool adaptive, int clip_limit, image::Image *mask) { + image_t src_img, mask_img; + convert_to_imlib_image(this, &src_img); + + if (adaptive) { + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_clahe_histeq(&src_img, clip_limit, &mask_img); + } else { + imlib_clahe_histeq(&src_img, clip_limit, NULL); + } + + } else { + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_histeq(&src_img, &mask_img); + } else { + imlib_histeq(&src_img, NULL); + } + } + return this; + } + + image::Image *Image::mean(int size, bool threshold, int offset, bool invert, image::Image *mask) { + image_t src_img, mask_img; + convert_to_imlib_image(this, &src_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_mean_filter(&src_img, size, threshold, offset, invert, &mask_img); + } else { + imlib_mean_filter(&src_img, size, threshold, offset, invert, NULL); + } + return this; + } + + image::Image *Image::median(int size, double percentile, bool threshold, int offset, bool invert, image::Image *mask) { + image_t src_img, mask_img; + convert_to_imlib_image(this, &src_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_median_filter(&src_img, size, percentile, threshold, offset, invert, &mask_img); + } else { + imlib_median_filter(&src_img, size, percentile, threshold, offset, invert, NULL); + } + + return this; + } + + image::Image *Image::mode(int size, bool threshold, int offset, bool invert, image::Image *mask) { + image_t src_img, mask_img; + convert_to_imlib_image(this, &src_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_mode_filter(&src_img, size, threshold, offset, invert, &mask_img); + } else { + imlib_mode_filter(&src_img, size, threshold, offset, invert, NULL); + } + return this; + } + + image::Image *Image::midpoint(int size, double bias, bool threshold, int offset, bool invert, image::Image *mask) { + image_t src_img, mask_img; + convert_to_imlib_image(this, &src_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_midpoint_filter(&src_img, size, bias, threshold, offset, invert, &mask_img); + } else { + imlib_midpoint_filter(&src_img, size, bias, threshold, offset, invert, NULL); + } + return this; + } + + image::Image *Image::morph(int size, std::vector kernel, float mul, float add, bool threshold, int offset, bool invert, image::Image *mask) { + image_t src_img, mask_img; + convert_to_imlib_image(this, &src_img); + + int *kernel_data = (int *)kernel.data(); + size_t len = kernel.size(); + + err::check_bool_raise(len != 0, "You need to config values of kernel"); + err::check_bool_raise(len == ((size * 2) + 1) * ((size * 2) + 1), "Kernel size not match"); + + int m = 0; + for (size_t i = 0; i < len; i++) { + m += kernel_data[i]; + } + if (m == 0) { + m = 1; + } + + if (mul < 0) { + mul = 1.0f / m; + } + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_morph(&src_img, size, kernel_data, mul, add, threshold, offset, invert, &mask_img); + } else { + imlib_morph(&src_img, size, kernel_data, mul, add, threshold, offset, invert, NULL); + } + return this; + } + + image::Image *Image::gaussian(int size, bool unsharp, float mul, float add, bool threshold, int offset, bool invert, image::Image *mask) { + std::vector pascal; + std::vector kernel; + int m = 0; + int k_2 = size * 2; + int n = k_2 + 1; + + pascal.resize(n); + pascal[0] = 1; + for (int i = 0; i < k_2; i ++) { + pascal[i + 1] = (pascal[i] * (k_2 - i)) / (i + 1); + } + + kernel.resize(n * n); + for (int i = 0; i < n; i ++) { + for (int j = 0; j < n; j ++) { + int temp = pascal[i] * pascal[j]; + kernel[(i * n) + j] = temp; + m += temp; + } + } + + if (unsharp == false) { + kernel[((n / 2) * n) + (n / 2)] -= m * 2; + m = -m; + } + + if (mul < 0) { + mul = 1.0f / m; + } + + image_t src_img, mask_img; + convert_to_imlib_image(this, &src_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_morph(&src_img, size, kernel.data(), mul, add, threshold, offset, invert, &mask_img); + } else { + imlib_morph(&src_img, size, kernel.data(), mul, add, threshold, offset, invert, NULL); + } + + return this; + } + + image::Image *Image::laplacian(int size, bool sharpen, float mul, float add, bool threshold, int offset, bool invert, image::Image *mask) { + std::vector pascal; + std::vector kernel; + int m = 0; + int k_2 = size * 2; + int n = k_2 + 1; + + pascal.resize(n); + pascal[0] = 1; + for (int i = 0; i < k_2; i ++) { + pascal[i + 1] = (pascal[i] * (k_2 - i)) / (i + 1); + } + + kernel.resize(n * n); + for (int i = 0; i < n; i ++) { + for (int j = 0; j < n; j ++) { + int temp = pascal[i] * pascal[j]; + kernel[(i * n) + j] = -temp; + m += temp; + } + } + + kernel[((n / 2) * n) + (n / 2)] += m; + m = kernel[((n / 2) * n) + (n / 2)]; + + if (sharpen == false) { + kernel[((n / 2) * n) + (n / 2)] += m; + } + + if (mul < 0) { + mul = 1.0f / m; + } + + image_t src_img, mask_img; + convert_to_imlib_image(this, &src_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_morph(&src_img, size, kernel.data(), mul, add, threshold, offset, invert, &mask_img); + } else { + imlib_morph(&src_img, size, kernel.data(), mul, add, threshold, offset, invert, NULL); + } + return this; + } + + image::Image *Image::bilateral(int size, double color_sigma, double space_sigma, bool threshold, int offset, bool invert, image::Image *mask) { + image_t src_img, mask_img; + convert_to_imlib_image(this, &src_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_bilateral_filter(&src_img, size, color_sigma, space_sigma, threshold, offset, invert, &mask_img); + } else { + imlib_bilateral_filter(&src_img, size, color_sigma, space_sigma, threshold, offset, invert, NULL); + } + return this; + } + + image::Image *Image::linpolar(bool reverse) { + image_t src_img; + convert_to_imlib_image(this, &src_img); + imlib_logpolar(&src_img, true, reverse); + return this; + } + + image::Image *Image::logpolar(bool reverse) { + image_t src_img; + convert_to_imlib_image(this, &src_img); + imlib_logpolar(&src_img, false, reverse); + return this; + } + + image::Image *Image::lens_corr(double strength, double zoom, double x_corr, double y_corr) { + if (_width % 2 || _height % 2) { + log::error("lens_corr image size must be even"); + return this; + } + + image_t src_img; + convert_to_imlib_image(this, &src_img); + imlib_lens_corr(&src_img, strength, zoom, x_corr, y_corr); + return this; + } + + image::Image *Image::rotation_corr(double x_rotation, double y_rotation, double z_rotation, double x_translation, double y_translation, double zoom, double fov, std::vector corners) { + image_t src_img; + convert_to_imlib_image(this, &src_img); + imlib_rotation_corr(&src_img, x_rotation, y_rotation, z_rotation, x_translation, y_translation, zoom, fov, (float *)corners.data()); + return this; + } + + std::map> Image::get_histogram(std::vector> thresholds, bool invert, std::vector roi, int bins, int l_bins, int a_bins, int b_bins, image::Image *difference) { + std::map> result = std::map>(); + image_t src_img, *other_img = NULL; + convert_to_imlib_image(this, &src_img); + if (difference) { + other_img = (image_t *)malloc(sizeof(image_t)); + if (!other_img) { + log::error("malloc image_t failed"); + return result; + } + convert_to_imlib_image(difference, other_img); + } + + rectangle_t roi_rect; + std::vector avail_roi = _get_available_roi(roi); + roi_rect.x = avail_roi[0]; + roi_rect.y = avail_roi[1]; + roi_rect.w = avail_roi[2]; + roi_rect.h = avail_roi[3]; + + list_t thresholds_list; + list_init(&thresholds_list, sizeof(color_thresholds_list_lnk_data_t)); + _convert_to_lab_thresholds(thresholds, &thresholds_list); + + histogram_t hist; + switch (_format) { + case image::FMT_GRAYSCALE: + // bins must be greater than 1 + bins = bins >= 2 ? bins : COLOR_GRAYSCALE_MAX - COLOR_GRAYSCALE_MIN + 1; + hist.LBinCount = bins; + hist.ABinCount = 0; + hist.BBinCount = 0; + hist.LBins = (float *)malloc(hist.LBinCount * sizeof(float)); + hist.ABins = NULL; + hist.BBins = NULL; + imlib_get_histogram(&hist, &src_img, &roi_rect, &thresholds_list, invert, other_img); + break; + case image::FMT_RGB888: + bins = bins >= 2 ? bins : COLOR_L_MAX - COLOR_L_MIN + 1; + l_bins = l_bins >= 2 ? l_bins : bins; + a_bins = a_bins >= 2 ? a_bins : COLOR_A_MAX - COLOR_A_MIN + 1; + b_bins = b_bins >= 2 ? b_bins : COLOR_B_MAX - COLOR_B_MIN + 1; + hist.LBinCount = l_bins; + hist.ABinCount = a_bins; + hist.BBinCount = b_bins; + hist.LBins = (float *)malloc(hist.LBinCount * sizeof(float)); + hist.ABins = (float *)malloc(hist.ABinCount * sizeof(float)); + hist.BBins = (float *)malloc(hist.BBinCount * sizeof(float)); + imlib_get_histogram(&hist, &src_img, &roi_rect, &thresholds_list, invert, other_img); + break; + default: + log::error("format not support: %d", _format); + return result; + } + + std::vector l_bins_data(hist.LBins, hist.LBins + hist.LBinCount); + std::vector a_bins_data(hist.ABins, hist.ABins + hist.ABinCount); + std::vector b_bins_data(hist.BBins, hist.BBins + hist.BBinCount); + result["L"] = l_bins_data; + result["A"] = a_bins_data; + result["B"] = b_bins_data; + + list_free(&thresholds_list); + if (difference && other_img) free(other_img); + if (hist.LBins) free(hist.LBins); + if (hist.ABins) free(hist.ABins); + if (hist.BBins) free(hist.BBins); + return result; + } + + image::Statistics Image::get_statistics(std::vector> thresholds, bool invert, std::vector roi, int bins, int l_bins, int a_bins, int b_bins, image::Image *difference) { + image::Statistics result = image::Statistics(); + image_t src_img, *other_img = NULL; + convert_to_imlib_image(this, &src_img); + if (difference) { + other_img = (image_t *)malloc(sizeof(image_t)); + if (!other_img) { + log::error("malloc image_t failed"); + return result; + } + convert_to_imlib_image(difference, other_img); + } + + rectangle_t roi_rect; + std::vector avail_roi = _get_available_roi(roi); + roi_rect.x = avail_roi[0]; + roi_rect.y = avail_roi[1]; + roi_rect.w = avail_roi[2]; + roi_rect.h = avail_roi[3]; + + list_t thresholds_list; + list_init(&thresholds_list, sizeof(color_thresholds_list_lnk_data_t)); + _convert_to_lab_thresholds(thresholds, &thresholds_list); + + histogram_t hist = {0}; + switch (_format) { + case image::FMT_GRAYSCALE: + // bins must be greater than 1 + bins = bins >= 2 ? bins : COLOR_GRAYSCALE_MAX - COLOR_GRAYSCALE_MIN + 1; + hist.LBinCount = bins; + hist.ABinCount = 0; + hist.BBinCount = 0; + hist.LBins = (float *)malloc(hist.LBinCount * sizeof(float)); + hist.ABins = NULL; + hist.BBins = NULL; + imlib_get_histogram(&hist, &src_img, &roi_rect, &thresholds_list, invert, other_img); + break; + case image::FMT_RGB888: + bins = bins >= 2 ? bins : COLOR_L_MAX - COLOR_L_MIN + 1; + l_bins = l_bins >= 2 ? l_bins : bins; + a_bins = a_bins >= 2 ? a_bins : COLOR_A_MAX - COLOR_A_MIN + 1; + b_bins = b_bins >= 2 ? b_bins : COLOR_B_MAX - COLOR_B_MIN + 1; + hist.LBinCount = l_bins; + hist.ABinCount = a_bins; + hist.BBinCount = b_bins; + hist.LBins = (float *)malloc(hist.LBinCount * sizeof(float)); + hist.ABins = (float *)malloc(hist.ABinCount * sizeof(float)); + hist.BBins = (float *)malloc(hist.BBinCount * sizeof(float)); + imlib_get_histogram(&hist, &src_img, &roi_rect, &thresholds_list, invert, other_img); + break; + default: + log::error("format not support: %d", _format); + return result; + } + + statistics_t stats = {0}; + imlib_get_statistics(&stats, (pixformat_t)src_img.pixfmt, &hist); + + std::vector l_statistics = {stats.LMean, stats.LMedian, stats.LMode, stats.LSTDev, stats.LMin, stats.LMax, stats.LLQ, stats.LUQ}; + std::vector a_statistics = {stats.AMean, stats.AMedian, stats.AMode, stats.ASTDev, stats.AMin, stats.AMax, stats.LLQ, stats.AUQ}; + std::vector b_statistics = {stats.BMean, stats.BMedian, stats.BMode, stats.BSTDev, stats.BMin, stats.BMax, stats.LLQ, stats.BUQ}; + result = image::Statistics( _format, + l_statistics, + a_statistics, + b_statistics); + + list_free(&thresholds_list); + if (difference && other_img) free(other_img); + if (hist.LBins) free(hist.LBins); + if (hist.ABins) free(hist.ABins); + if (hist.BBins) free(hist.BBins); + return result; + } + + std::vector Image::get_regression(std::vector> thresholds, bool invert, std::vector roi, int x_stride, int y_stride, int area_threshold, int pixels_threshold, bool robust) { + std::vector lines = std::vector(); + image_t src_img; + if (_format != image::FMT_GRAYSCALE && _format != image::FMT_RGB888 && _format != image::FMT_RGB565) { + log::error("get_regression only support GRAYSCALE RGB888 RGB565 format!\n"); + } + convert_to_imlib_image(this, &src_img); + + rectangle_t roi_rect; + std::vector avail_roi = _get_available_roi(roi); + roi_rect.x = avail_roi[0]; + roi_rect.y = avail_roi[1]; + roi_rect.w = avail_roi[2]; + roi_rect.h = avail_roi[3]; + + list_t thresholds_list; + list_init(&thresholds_list, sizeof(color_thresholds_list_lnk_data_t)); + _convert_to_lab_thresholds(thresholds, &thresholds_list); + + find_lines_list_lnk_data_t out; + bool res = imlib_get_regression(&out, &src_img, &roi_rect, x_stride, + y_stride, &thresholds_list, invert, area_threshold, pixels_threshold, robust); + if (true == res) { + Line line = Line(out.line.x1, out.line.y1, out.line.x2, out.line.y2, out.magnitude, out.theta, out.rho); + lines.push_back(line); + } + + list_free(&thresholds_list); + return lines; + } + + image::Image *Image::flood_fill(int x, int y, float seed_threshold, float floating_threshold, image::Color color , bool invert, bool clear_background, image::Image *mask) { + image_t src_img, mask_img; + convert_to_imlib_image(this, &src_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_flood_fill(&src_img, x, y, seed_threshold, floating_threshold, color.hex(), invert, clear_background, &mask_img); + } else { + imlib_flood_fill(&src_img, x, y, seed_threshold, floating_threshold, color.hex(), invert, clear_background, NULL); + } + return this; + } + + image::Image *Image::erode(int size, int threshold, image::Image *mask) { + err::check_bool_raise(size > 0, "erode size must be greater than 0"); + err::check_bool_raise(threshold == -1 || threshold >= 0, "erode threshold must be greater than or equal to 0"); + + image_t src_img, mask_img; + convert_to_imlib_image(this, &src_img); + + if (threshold == -1) { + threshold = ((size * 2) + 1) * ((size * 2) + 1) - 1; + } + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_erode(&src_img, size, threshold, &mask_img); + } else { + imlib_erode(&src_img, size, threshold, NULL); + } + return this; + } + + image::Image *Image::dilate(int size, int threshold, image::Image *mask) { + err::check_bool_raise(size > 0, "dilate size must be greater than 0"); + err::check_bool_raise(threshold >= 0, "dilate threshold must be greater than or equal to 0"); + + image_t src_img, mask_img; + convert_to_imlib_image(this, &src_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_dilate(&src_img, size, threshold, &mask_img); + } else { + imlib_dilate(&src_img, size, threshold, NULL); + } + return this; + } + + image::Image *Image::open(int size, int threshold, image::Image *mask) { + err::check_bool_raise(size > 0, "open size must be greater than 0"); + err::check_bool_raise(threshold >= 0, "open threshold must be greater than or equal to 0"); + + image_t src_img, mask_img; + convert_to_imlib_image(this, &src_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_open(&src_img, size, threshold, &mask_img); + } else { + imlib_open(&src_img, size, threshold, NULL); + } + return this; + } + + image::Image *Image::close(int size, int threshold, image::Image *mask) { + err::check_bool_raise(size > 0, "close size must be greater than 0"); + err::check_bool_raise(threshold >= 0, "close threshold must be greater than or equal to 0"); + + image_t src_img, mask_img; + convert_to_imlib_image(this, &src_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_close(&src_img, size, threshold, &mask_img); + } else { + imlib_close(&src_img, size, threshold, NULL); + } + return this; + } + + image::Image *Image::top_hat(int size, int threshold, image::Image *mask) { + err::check_bool_raise(size > 0, "top_hat size must be greater than 0"); + err::check_bool_raise(threshold >= 0, "top_hat threshold must be greater than or equal to 0"); + + image_t src_img, mask_img; + convert_to_imlib_image(this, &src_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_top_hat(&src_img, size, threshold, &mask_img); + } else { + imlib_top_hat(&src_img, size, threshold, NULL); + } + return this; + } + + image::Image *Image::black_hat(int size, int threshold, image::Image *mask) { + err::check_bool_raise(size > 0, "black_hat size must be greater than 0"); + err::check_bool_raise(threshold >= 0, "black_hat threshold must be greater than or equal to 0"); + + image_t src_img, mask_img; + convert_to_imlib_image(this, &src_img); + + if (mask) { + convert_to_imlib_image(mask, &mask_img); + imlib_black_hat(&src_img, size, threshold, &mask_img); + } else { + imlib_black_hat(&src_img, size, threshold, NULL); + } + return this; + } +} \ No newline at end of file diff --git a/vision/kvm_vision_test/main/CMakeLists.txt b/vision/kvm_vision_test/main/CMakeLists.txt new file mode 100644 index 0000000..3053d5b --- /dev/null +++ b/vision/kvm_vision_test/main/CMakeLists.txt @@ -0,0 +1,74 @@ +############### Add include ################### +list(APPEND ADD_INCLUDE "include" + ) +list(APPEND ADD_PRIVATE_INCLUDE "") +############################################### + +############ Add source files ################# +# list(APPEND ADD_SRCS "src/main.c" +# "src/test.c" +# ) +append_srcs_dir(ADD_SRCS "src") # append source file in src dir to var ADD_SRCS +# list(REMOVE_ITEM COMPONENT_SRCS "src/test2.c") +# FILE(GLOB_RECURSE EXTRA_SRC "src/*.c") +# FILE(GLOB EXTRA_SRC "src/*.c") +# list(APPEND ADD_SRCS ${EXTRA_SRC}) +# aux_source_directory(src ADD_SRCS) # collect all source file in src dir, will set var ADD_SRCS +# append_srcs_dir(ADD_SRCS "src") # append source file in src dir to var ADD_SRCS +# list(REMOVE_ITEM COMPONENT_SRCS "src/test.c") +# set(ADD_ASM_SRCS "src/asm.S") +# list(APPEND ADD_SRCS ${ADD_ASM_SRCS}) +# SET_PROPERTY(SOURCE ${ADD_ASM_SRCS} PROPERTY LANGUAGE C) # set .S ASM file as C language +# SET_SOURCE_FILES_PROPERTIES(${ADD_ASM_SRCS} PROPERTIES COMPILE_FLAGS "-x assembler-with-cpp -D BBBBB") +############################################### + +###### Add required/dependent components ###### +list(APPEND ADD_REQUIREMENTS basic peripheral kvm) +############################################### + +###### Add link search path for requirements/libs ###### +# list(APPEND ADD_LINK_SEARCH_PATH "${CONFIG_TOOLCHAIN_PATH}/lib") +# list(APPEND ADD_REQUIREMENTS pthread m) # add system libs, pthread and math lib for example here +# set (OpenCV_DIR opencv/lib/cmake/opencv4) +# find_package(OpenCV REQUIRED) +############################################### + +############ Add static libs ################## +# list(APPEND ADD_STATIC_LIB "lib/libtest.a") +############################################### + +#### Add compile option for this component #### +#### Just for this component, won't affect other +#### modules, including component that depend +#### on this component +# list(APPEND ADD_DEFINITIONS_PRIVATE -DAAAAA=1) + +#### Add compile option for this component +#### and components denpend on this component +# list(APPEND ADD_DEFINITIONS -DAAAAA222=1 +# -DAAAAA333=1) +############################################### + +############ Add static libs ################## +#### Update parent's variables like CMAKE_C_LINK_FLAGS +# set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--start-group libmaix/libtest.a -ltest2 -Wl,--end-group" PARENT_SCOPE) +############################################### + +######### Add files need to download ######### +# list(APPEND ADD_FILE_DOWNLOADS "{ +# 'url': 'https://*****/abcde.tar.xz', +# 'urls': [], # backup urls, if url failed, will try urls +# 'sites': [], # download site, user can manually download file and put it into dl_path +# 'sha256sum': '', +# 'filename': 'abcde.tar.xz', +# 'path': 'toolchains/xxxxx', +# 'check_files': [] +# }" +# ) +# +# then extracted file in ${DL_EXTRACTED_PATH}/toolchains/xxxxx, +# you can directly use then, for example use it in add_custom_command +############################################## + +# register component, DYNAMIC or SHARED flags will make component compiled to dynamic(shared) lib +register_component() diff --git a/vision/kvm_vision_test/main/Kconfig b/vision/kvm_vision_test/main/Kconfig new file mode 100644 index 0000000..e69de29 diff --git a/vision/kvm_vision_test/main/src/main.cpp b/vision/kvm_vision_test/main/src/main.cpp new file mode 100644 index 0000000..59b03d2 --- /dev/null +++ b/vision/kvm_vision_test/main/src/main.cpp @@ -0,0 +1,79 @@ +#include "kvm_vision.h" +#include "maix_basic.hpp" + +using namespace maix; +using namespace maix::sys; + +// #define NOT_GET_IMG + +int main(int argc, char* argv[]) +{ + uint64_t __attribute__((unused)) start_time; + // Catch SIGINT signal(e.g. Ctrl + C), and set exit flag to true. + signal(SIGINT, [](int sig){ app::set_exit_flag(true); + log::info("========================\n"); + }); + + kvmv_hdmi_control(0); + kvmv_hdmi_control(1); + + kvmv_init(0); + set_h264_gop(30); + + uint16_t get_fream_count = 0; + + while(!app::need_exit()){ +#ifdef NOT_GET_IMG + printf("NOT_GET_IMG ...\n"); + time::sleep_ms(1000); +#else + uint8_t* p_kvmv_img_data; + uint32_t kvmv_img_data_size; + int ret; + + printf("KVM-Vison Get Fream ...\n"); + + start_time = time::time_ms(); + if(get_fream_count < 1){ + get_fream_count ++; + ret = kvmv_read_img(1920, 1080, 1, 3000, &p_kvmv_img_data, &kvmv_img_data_size); + } else if(get_fream_count >= 1 && get_fream_count < 2){ + get_fream_count ++; + ret = kvmv_read_img(0, 0, 0, 60, &p_kvmv_img_data, &kvmv_img_data_size); + } else { + get_fream_count = 0; + } + + // printf("kvmv_read_img(): %d \r\n", (int)(time::time_ms() - start_time)); + + // ret = kvmv_read_img(1920, 1080, 1, 3000, &p_kvmv_img_data, &kvmv_img_data_size); + // ret = kvmv_read_img(1920, 1080, 0, 60, &p_kvmv_img_data, &kvmv_img_data_size); + + printf("kvmv_read_img ret = %d\n", ret); + if(ret == IMG_H264_TYPE_IF){ + uint8_t* p_kvmv_sps_data; + uint8_t* p_kvmv_pps_data; + uint32_t kvmv_sps_data_size; + uint32_t kvmv_pps_data_size; + + ret = kvmv_get_sps_frame(&p_kvmv_sps_data, &kvmv_sps_data_size); + if(ret == IMG_H264_TYPE_SPS){ + printf("Get SPS data size = %d\n", kvmv_sps_data_size); + } else { + printf("Get SPS data error!\n"); + } + ret = kvmv_get_pps_frame(&p_kvmv_pps_data, &kvmv_pps_data_size); + if(ret == IMG_H264_TYPE_PPS){ + printf("Get PPS data size = %d\n", kvmv_pps_data_size); + } else { + printf("Get PPS data error!\n"); + } + } + // send... + // if(ret >= 0){ + free_kvmv_data(&p_kvmv_img_data); + // } +#endif + } + kvmv_deinit(); +} \ No newline at end of file diff --git a/web/README.md b/web/README.md index e2dab9f..4c385df 100644 --- a/web/README.md +++ b/web/README.md @@ -22,6 +22,8 @@ src ## Local Development +> Development requires SSH. You can enable it in the Web Settings: `Settings > SSH`. + Due to CORS restrictions, authentication needs to be disabled during local development. To develop authentication features, you need to build the project and test in NanoKVM. @@ -55,4 +57,7 @@ pnpm build 3. Upload `web` to `/kvmapp/server/` in NanoKVM. 4. Restart the service by executing `/etc/init.d/S95nanokvm restart` in NanoKVM. -Additionally, browser may have old version cache. If you can't open the page, try a force refresh or clear the cache. +Tips: + +1. File uploads requires SSH. You can enable it in the Web Settings: `Settings > SSH`. +2. Browser may have old version cache. If you can't open the page, try a force refresh or clear the cache. diff --git a/web/README_ZH.md b/web/README_ZH.md index f4c68c5..331ef60 100644 --- a/web/README_ZH.md +++ b/web/README_ZH.md @@ -22,11 +22,13 @@ src ## 本地开发 +> 开发需要启用 SSH 功能。请在 Web `设置 - SSH` 中检查 SSH 是否已经启用。 + 由于 CORS 的限制,在本地开发时,需要关闭鉴权功能。 如果想要开发鉴权相关的功能,需要编译后在 NanoKVM 中进行测试。 -1. 通过 ssh 登录到 NanoKVM:`ssh root@your-nanokvm-ip`(默认密码为 root); +1. 通过 SSH 登录到 NanoKVM:`ssh root@your-nanokvm-ip`(默认密码为 root); 2. 修改配置文件 `/etc/kvm/server.yaml`,添加一行 `authentication: disable`。⚠️注意:该选项会禁用所有鉴权功能,生产环境请勿开启该选项! 3. 执行 `/etc/init.d/S95nanokvm restart` 重启服务。 4. 编辑 `.env.development` 文件,将 `VITE_SERVER_IP` 修改为你的 NanoKVM IP 地址。 @@ -56,4 +58,7 @@ pnpm build 4. 在 NanoKVM 中执行 `/etc/init.d/S95nanokvm restart` 重启服务。 -注意:更新 web 目录后,浏览器可能会有缓存。如果遇到打不开页面的情况,请强制刷新或清空缓存。 +注意: + +1. 上传文件需要启用 SSH 功能。请在 Web `设置 - SSH` 中检查 SSH 是否已经启用。 +2. 更新 web 目录后,浏览器可能会有缓存。如果遇到打不开页面的情况,请强制刷新或清空缓存。 diff --git a/web/src/api/auth.ts b/web/src/api/auth.ts index 0741cb6..3657ce4 100644 --- a/web/src/api/auth.ts +++ b/web/src/api/auth.ts @@ -8,6 +8,10 @@ export function login(username: string, password: string) { return http.post('/api/auth/login', data); } +export function logout() { + return http.post('/api/auth/logout'); +} + export function getAccount() { return http.get('/api/auth/account'); } diff --git a/web/src/api/extensions/tailscale.ts b/web/src/api/extensions/tailscale.ts new file mode 100644 index 0000000..556a0b9 --- /dev/null +++ b/web/src/api/extensions/tailscale.ts @@ -0,0 +1,51 @@ +import { http } from '@/lib/http.ts'; + +// install tailscale +export function install() { + return http.post('/api/extensions/tailscale/install'); +} + +// uninstall tailscale +export function uninstall() { + return http.post('/api/extensions/tailscale/uninstall'); +} + +// get tailscale status +export function getStatus() { + return http.get('/api/extensions/tailscale/status'); +} + +// start tailscale +export function start() { + return http.post('/api/extensions/tailscale/start'); +} + +// restart tailscale +export function restart() { + return http.post('/api/extensions/tailscale/restart'); +} + +// stop tailscale +export function stop() { + return http.post('/api/extensions/tailscale/stop'); +} + +// run tailscale up +export function up() { + return http.post('/api/extensions/tailscale/up'); +} + +// run tailscale down +export function down() { + return http.post('/api/extensions/tailscale/down'); +} + +// login tailscale +export function login() { + return http.post('/api/extensions/tailscale/login'); +} + +// logout tailscale +export function logout() { + return http.post('/api/extensions/tailscale/logout'); +} diff --git a/web/src/api/network.ts b/web/src/api/network.ts index babfa5a..6c60474 100644 --- a/web/src/api/network.ts +++ b/web/src/api/network.ts @@ -21,51 +21,6 @@ export function deleteWolMac(mac: string) { }); } -// install tailscale -export function installTailscale() { - return http.post('/api/network/tailscale/install'); -} - -// uninstall tailscale -export function uninstallTailscale() { - return http.post('/api/network/tailscale/uninstall'); -} - -// get tailscale status -export function getTailscaleStatus() { - return http.get('/api/network/tailscale/status'); -} - -// run tailscale up -export function upTailscale() { - return http.post('/api/network/tailscale/up'); -} - -// run tailscale down -export function downTailscale() { - return http.post('/api/network/tailscale/down'); -} - -// login tailscale -export function loginTailscale() { - return http.post('/api/network/tailscale/login'); -} - -// logout tailscale -export function logoutTailscale() { - return http.post('/api/network/tailscale/logout'); -} - -// stop tailscale -export function stopTailscale() { - return http.post('/api/network/tailscale/stop'); -} - -// restart tailscale -export function restartTailscale() { - return http.post('/api/network/tailscale/restart'); -} - // get wifi information export function getWiFi() { return http.get('/api/network/wifi'); diff --git a/web/src/api/storage.ts b/web/src/api/storage.ts index b9dcdc8..9f6c1b8 100644 --- a/web/src/api/storage.ts +++ b/web/src/api/storage.ts @@ -11,9 +11,15 @@ export function getMountedImage() { } // mount/unmount image -export function mountImage(file?: string) { +export function mountImage(file?: string, cdrom?: boolean) { const data = { - file: file ? file : '' + file: file ? file : '', + cdrom: cdrom }; return http.post('/api/storage/image/mount', data); } + +// get CD-ROM flag +export function getCdRom() { + return http.get('/api/storage/cdrom'); +} diff --git a/web/src/api/vm.ts b/web/src/api/vm.ts index 7e2f6d1..61b9060 100644 --- a/web/src/api/vm.ts +++ b/web/src/api/vm.ts @@ -61,3 +61,18 @@ export function setOLED(sleep: number) { export function resetHdmi() { return http.post('/api/vm/hdmi/reset'); } + +// get SSH state +export function getSSHState() { + return http.get('/api/vm/ssh'); +} + +// enable SSH +export function enableSSH() { + return http.post('/api/vm/ssh/enable'); +} + +// disable SSH +export function disableSSH() { + return http.post('/api/vm/ssh/disable'); +} diff --git a/web/src/components/main-error.tsx b/web/src/components/main-error.tsx index 4957027..ca40fcf 100644 --- a/web/src/components/main-error.tsx +++ b/web/src/components/main-error.tsx @@ -1,4 +1,5 @@ import { Button } from 'antd'; +import { t } from 'i18next'; export const MainError = () => { return ( @@ -7,10 +8,10 @@ export const MainError = () => { role="alert" >

- Sorry, there seems to be some problems :( + {t('error.title')}

); diff --git a/web/src/i18n/languages.ts b/web/src/i18n/languages.ts index 9ba47c1..ff2b501 100644 --- a/web/src/i18n/languages.ts +++ b/web/src/i18n/languages.ts @@ -17,7 +17,8 @@ const languages = [ { key: 'ja', name: '日本語' }, { key: 'cz', name: 'Česky' }, { key: 'uk', name: 'Українська' }, - { key: 'nb', name: 'Norsk, bokmål' } + { key: 'nb', name: 'Norsk, bokmål' }, + { key: 'th', name: 'ภาษาไทย' } ]; languages.sort((a, b) => a.name.localeCompare(b.name, 'en', { sensitivity: 'base' })); diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index ce98f97..d776aa6 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -28,7 +28,7 @@ const en = { loginButtonText: 'Login', tips: { reset1: - 'To reset the passwords, pressing and holding the BOOT button on the NanoKVM for 10 seconds.', + 'To reset the passwords, press and hold the BOOT button on the NanoKVM for 10 seconds.', reset2: 'For detailed steps, please consult this document:', reset3: 'Web default account:', reset4: 'SSH default account:', @@ -89,6 +89,7 @@ const en = { title: 'Image', loading: 'Loading...', empty: 'Nothing Found', + cdrom: 'Mount the image in CD-ROM mode', mountFailed: 'Mount Failed', mountDesc: "In some systems, it's necessary to eject the virtual disk on the remote host before mounting the image.", @@ -115,7 +116,7 @@ const en = { runBackground: 'Run Background', runFailed: 'Run failed', attention: 'Attention', - delDesc: 'Are you sure to delete this file?', + delDesc: 'Are you sure you want to delete this file?', confirm: 'Yes', cancel: 'No', delete: 'Delete', @@ -137,6 +138,12 @@ const en = { input: 'Please enter the MAC', ok: 'Ok' }, + download: { + title: 'Download Image', + input: 'Please enter a remote image URL', + ok: 'Ok', + disabled: '/data partition is RO, so we cannot download the image' + }, power: { title: 'Power', reset: 'Reset', @@ -144,12 +151,6 @@ const en = { powerShort: 'Power (short click)', powerLong: 'Power (long click)' }, - download: { - download: 'Download Image', - input: 'Please enter a remote image URL', - ok: 'Ok', - disabled: '/data partition is RO, so we cannot download the image', - }, settings: { title: 'Settings', about: { @@ -175,7 +176,7 @@ const en = { title: 'Device', oled: { title: 'OLED', - description: 'OLED screen automatically sleep', + description: 'Turn off OLED screen after', 0: 'Never', 15: '15 sec', 30: '30 sec', @@ -191,6 +192,10 @@ const en = { description: 'Configure Wi-Fi', setBtn: 'Config' }, + ssh: { + description: 'Enable SSH remote access', + tip: 'Set a strong password before enabling (Account - Change Password)' + }, disk: 'Virtual Disk', diskDesc: 'Mount virtual U-disk on the remote host', network: 'Virtual Network', @@ -200,12 +205,12 @@ const en = { title: 'Tailscale', memory: { title: 'Memory optimization', - tip: "When memory usage exceeds the limit, garbage collection is performed more aggressively to attempt to free up memory. it's recommended to set to 50MB if using Tailscale. A Tailscale restart is required for the change to take effect.", + tip: "When memory usage exceeds the limit, garbage collection is performed more aggressively to attempt to free up memory. It's recommended to set to 50MB if using Tailscale. A Tailscale restart is required for the change to take effect.", disable: 'Disable' }, - restart: 'Are you sure to restart Tailscale?', - stop: 'Are you sure to stop Tailscale?', - stopDesc: 'Log out Tailscale and disable its automatic startup on boot.', + restart: 'Restart Tailscale?', + stop: 'Stop Tailscale?', + stopDesc: 'Log out Tailscale and disable automatic startup on boot.', loading: 'Loading...', notInstall: 'Tailscale not found! Please install.', install: 'Install', @@ -228,7 +233,8 @@ const en = { deviceIP: 'Device IP', account: 'Account', logout: 'Logout', - logout2: 'Sure to logout?', + logout2: 'Confirm logout', + uninstall: 'Uninstall Tailscale', okBtn: 'Yes', cancelBtn: 'No' }, @@ -237,7 +243,7 @@ const en = { queryFailed: 'Get version failed', updateFailed: 'Update failed. Please retry.', isLatest: 'You already have the latest version.', - available: 'An update is available. Are you sure to update?', + available: 'An update is available. Are you sure you want to update now?', updating: 'Update started. Please wait...', confirm: 'Confirm', cancel: 'Cancel' @@ -246,9 +252,13 @@ const en = { title: 'Account', webAccount: 'Web Account Name', password: 'Password', - updateBtn: 'Update', + updateBtn: 'Change', logoutBtn: 'Logout' } + }, + error: { + title: "We've ran into an issue", + refresh: 'Refresh' } } }; diff --git a/web/src/i18n/locales/fr.ts b/web/src/i18n/locales/fr.ts index 1bd1813..70e62aa 100644 --- a/web/src/i18n/locales/fr.ts +++ b/web/src/i18n/locales/fr.ts @@ -30,23 +30,23 @@ const fr = { loginButtonText: 'Connexion', tips: { reset1: - 'To reset the passwords, pressing and holding the BOOT button on the NanoKVM for 10 seconds.', - reset2: 'For detailed steps, please consult this document:', - reset3: 'Web default account:', - reset4: 'SSH default account:', - change1: 'Please note that this action will change the following passwords:', - change2: 'Web login password', - change3: 'System root password (SSH login password)', - change4: 'To reset the passwords, press and hold the BOOT button on the NanoKVM.' + 'Pour réinitialiser les mots de passe, appuyez et maintenez enfoncé le bouton BOOT sur le NanoKVM pendant 10 secondes.', + reset2: 'Pour les étapes détaillées, veuillez consulter ce document :', + reset3: 'Compte Web par défaut :', + reset4: 'Compte SSH par défaut :', + change1: 'Veuillez noter que cette action modifiera les mots de passe suivants :', + change2: 'Mot de passe de connexion Web', + change3: 'Mot de passe racine du système (mot de passe de connexion SSH)', + change4: 'Pour réinitialiser les mots de passe, appuyez et maintenez enfoncé le bouton BOOT sur le NanoKVM.' } }, wifi: { title: 'Wi-Fi', - description: 'Configure Wi-Fi for NanoKVM', - success: 'Please check the network status of NanoKVM and visit the new IP address.', - failed: 'Operation failed, please try again.', + description: 'Configurez le Wi-Fi pour le NanoKVM', + success: 'Veuillez vérifier le statut du réseau du NanoKVM et visitez la nouvelle adresse IP.', + failed: 'L\'opération a échoué, veuillez réessayer.', confirmBtn: 'Ok', - finishBtn: 'Finished' + finishBtn: 'Terminé' }, screen: { video: 'Mode vidéo', @@ -61,10 +61,10 @@ const fr = { qualityHigh: 'Élevé', qualityMedium: 'Moyen', qualityLow: 'Bas', - frameDetect: 'Frame Detect', + frameDetect: 'Détection de trame', frameDetectTip: "Calcule la différence entre les images. Arrête la transmission du flux vidéo lorsqu'aucun changement n'est détecté sur l'écran de l'hôte distant", - resetHdmi: 'Reset HDMI' + resetHdmi: 'Réinitialiser le HDMI' }, keyboard: { paste: 'Coller', @@ -85,7 +85,7 @@ const fr = { absolute: 'Mode absolu', relative: 'Mode relatif', requestPointer: - 'Utilisation du mode relatif. Veuillez cliquer sur le bureau pour obtenir le pointeur de la souris.', + 'Pour utiliser le mode relatif, cliquez sur le bureau pour capturer le pointeur de la souris.', resetHid: 'Réinitialiser le périphérique HID' }, image: { @@ -101,7 +101,7 @@ const fr = { usb2: 'Assurez-vous que le disque virtuel est monté (Paramètres - Disque virtuel).', usb3: 'Ouvrez le disque virtuel sur votre ordinateur et copiez le fichier image dans le répertoire racine du disque virtuel.', scp1: 'Assurez-vous que le NanoKVM et votre ordinateur sont sur le même réseau local.', - scp2: 'Ouvrez un terminal sur votre ordinateur et utilisez la commande SCP pour télécharger le fichier image dans le répertoire /data du NanoKVM.', + scp2: 'Ouvrez un terminal sur votre ordinateur et utilisez la commande SCP pour copier le fichier image dans le répertoire /data du NanoKVM.', scp3: 'Exemple : scp chemin-de-votre-image root@ip-de-votre-nanokvm:/data', tfCard: 'Carte TF', tf1: 'Cette méthode est adaptée aux systèmes Linux.', @@ -114,11 +114,11 @@ const fr = { script: { title: 'Script', upload: 'Téléverser', - run: 'Executer', - runBackground: 'Executer en arrière-plan', + run: 'Exécuter', + runBackground: 'Exécuter en arrière-plan', runFailed: "Échec de l'exécution", attention: 'Attention', - delDesc: 'Etes-vous sûr de vouloir supprimer ce fichier?', + delDesc: 'Êtes-vous sûr de vouloir supprimer ce fichier ?', confirm: 'Oui', cancel: 'Non', delete: 'Supprimer', @@ -142,38 +142,38 @@ const fr = { }, power: { title: 'Power', - reset: 'Reset', + reset: 'Réinitialiser', power: 'Power', powerShort: 'Power (appui court)', powerLong: 'Power (appui long)' }, settings: { - title: 'Settings', + title: 'Paramètres', about: { title: 'A propos de NanoKVM', information: 'Informations', ip: 'IP', mdns: 'mDNS', application: "Version de l'application", - applicationTip: 'NanoKVM web application version', + applicationTip: 'Version de l\'application Web NanoKVM', image: "Version de l'image", - imageTip: 'NanoKVM system image version', + imageTip: 'Version de l\'image système NanoKVM', deviceKey: "Clé de l'appareil", community: 'Communauté' }, appearance: { - title: 'Appearance', - display: 'Display', - language: 'Language', - menuBar: 'Menu Bar', - menuBarDesc: 'Display icons in the menu bar' + title: 'Apparence', + display: 'Affichage', + language: 'Langue', + menuBar: 'Barre de menus', + menuBarDesc: 'Afficher les icônes dans la barre de menus' }, device: { - title: 'Device', + title: 'Appareil', oled: { title: 'OLED', - description: 'OLED screen automatically sleep', - 0: 'Never', + description: 'Écran OLED s\'éteint automatiquement', + 0: 'Jamais', 15: '15 sec', 30: '30 sec', 60: '1 min', @@ -181,36 +181,36 @@ const fr = { 300: '5 min', 600: '10 min', 1800: '30 min', - 3600: '1 hour' + 3600: '1 heure' }, wifi: { title: 'Wi-Fi', - description: 'Configure Wi-Fi', - setBtn: 'Config' + description: 'Configurez le Wi-Fi', + setBtn: 'Configurer' }, - disk: 'Virtual Disk', - diskDesc: 'Mount virtual U-disk on the remote host', - network: 'Virtual Network', - networkDesc: 'Mount virtual network card on the remote host' + disk: 'Disque virtuel', + diskDesc: 'Monter le disque virtuel U sur l\'hôte distant', + network: 'Réseau virtuel', + networkDesc: 'Monter la carte réseau virtuelle sur l\'hôte distant' }, tailscale: { title: 'Tailscale', memory: { - title: 'Memory optimization', - tip: "When memory usage exceeds the limit, garbage collection is performed more aggressively to attempt to free up memory. it's recommended to set to 50MB if using Tailscale. A Tailscale restart is required for the change to take effect.", - disable: 'Disable' + title: 'Optimisation de la mémoire', + tip: "Lorsque l'utilisation de la mémoire dépasse la limite, la collecte des ordures est effectuée plus agressivement pour essayer de libérer de la mémoire. Il est recommandé de définir à 50MB si vous utilisez Tailscale. Un redémarrage de Tailscale est nécessaire pour que le changement prenne effet.", + disable: 'Désactiver' }, - restart: 'Are you sure to restart Tailscale?', - stop: 'Are you sure to stop Tailscale?', - stopDesc: 'Log out Tailscale and disable its automatic startup on boot.', + restart: 'Êtes-vous sûr de vouloir redémarrer Tailscale ?', + stop: 'Êtes-vous sûr de vouloir arrêter Tailscale ?', + stopDesc: 'Arrêtez Tailscale et désactivez son démarrage automatique.', loading: 'Chargement...', - notInstall: "Tailscale non trouvé! Veuillez l'installer.", + notInstall: "Tailscale non trouvé ! Veuillez l'installer.", install: 'Installer', installing: 'Installation', failed: 'Installation échouée', retry: "Veuillez rafraîchir et réessayer. Ou essayez d'installer manuellement", download: 'Télécharger le', - package: 'installation package', + package: 'paquet d\'installation', unzip: 'et décompressez-le', upTailscale: 'Téléverser tailscale dans le répertoire NanoKVM /usr/sbin/', upTailscaled: 'Téléverser tailscaled dans le répertoire NanoKVM /usr/sbin/', @@ -225,13 +225,13 @@ const fr = { account: 'Compte', logout: 'Déconnexion', logout2: 'Voulez-vous vous déconnecter?', - okBtn: 'Yes', - cancelBtn: 'No' + okBtn: 'Oui', + cancelBtn: 'Non' }, update: { title: 'Vérifier les mises à jour', queryFailed: 'Impossible de vérifier les mises à jour. Veuillez réessayer.', - updateFailed: 'Mis à jour échouée. Veuillez réessayer.', + updateFailed: 'Mise à jour échouée. Veuillez réessayer.', isLatest: 'Vous avez déjà la dernière version.', available: 'Une mise à jour est disponible. Voulez-vous vraiment mettre à jour?', updating: 'Mise à jour en cours. Veuillez patienter...', @@ -239,11 +239,11 @@ const fr = { cancel: 'Annuler' }, account: { - title: 'Account', - webAccount: 'Web Account Name', - password: 'Password', - updateBtn: 'Update', - logoutBtn: 'Logout' + title: 'Compte', + webAccount: 'Nom du compte Web', + password: 'Mot de passe', + updateBtn: 'Mettre à jour', + logoutBtn: 'Déconnexion' } } } diff --git a/web/src/i18n/locales/ko.ts b/web/src/i18n/locales/ko.ts index f2b41b8..d4f8805 100644 --- a/web/src/i18n/locales/ko.ts +++ b/web/src/i18n/locales/ko.ts @@ -137,6 +137,12 @@ const ko = { input: 'MAC주소를 입력하세요.', ok: '확인' }, + download: { + title: '이미지 다운로드', + input: '원격 이미지 URL을 입력하세요.', + ok: '확인', + disabled: '/data 파티션이 읽기 전용(RO) 상태이므로 이미지를 다운로드할 수 없습니다.' + }, power: { title: '전원', reset: '리셋', @@ -144,12 +150,6 @@ const ko = { powerShort: '전원 (짧게 누르기)', powerLong: '전원 (길게 누르기)' }, - download: { - download: '이미지 다운로드', - input: '원격 이미지 URL을 입력하세요.', - ok: '확인', - disabled: '/data 파티션이 읽기 전용(RO) 상태이므로 이미지를 다운로드할 수 없습니다.' - }, settings: { title: '설정', about: { @@ -191,6 +191,10 @@ const ko = { description: 'Wi-Fi 설정', setBtn: '설정' }, + ssh: { + description: 'SSH 원격 접속 활성화', + tip: '활성화하기 전에 강력한 비밀번호를 설정하세요. (계정 - 비밀번호 변경)' + }, disk: '가상 디스크', diskDesc: '원격 호스트에서 가상 U-디스크를 마운트합니다.', network: '가상 네트워크', @@ -228,6 +232,7 @@ const ko = { account: '계정', logout: '로그아웃', logout2: '정말로 로그아웃 합니까?', + uninstall: 'Tailscale 제거', okBtn: '예', cancelBtn: '아니오' }, diff --git a/web/src/i18n/locales/ru.ts b/web/src/i18n/locales/ru.ts index ed81156..cf3d893 100644 --- a/web/src/i18n/locales/ru.ts +++ b/web/src/i18n/locales/ru.ts @@ -19,8 +19,7 @@ const ru = { invalidUser: 'Неверное имя пользователя или пароль', error: 'Непредвиденная ошибка', changePassword: 'Изменить пароль', - changePasswordDesc: - 'Для безопасности вашего устройства измените, пожалуйста, пароль веб-входа.', + changePasswordDesc: 'Для безопасности вашего устройства, пожалуйста поменяйте пароль!', differentPassword: 'Пароли не совпадают', illegalUsername: 'Имя пользователя содержит недопустимые символы', illegalPassword: 'Пароль содержит недопустимые символы', @@ -30,23 +29,23 @@ const ru = { loginButtonText: 'Войти', tips: { reset1: - 'To reset the passwords, pressing and holding the BOOT button on the NanoKVM for 10 seconds.', - reset2: 'For detailed steps, please consult this document:', - reset3: 'Web default account:', - reset4: 'SSH default account:', - change1: 'Please note that this action will change the following passwords:', - change2: 'Web login password', - change3: 'System root password (SSH login password)', - change4: 'To reset the passwords, press and hold the BOOT button on the NanoKVM.' + 'Чтобы сбросить пароли, нажмите и удерживайте кнопку BOOT на NanoKVM в течении 10 секунд.', + reset2: 'Подробные шаги описаны в этом документе:', + reset3: 'Аккаунт веб-интерфейса по умолчанию:', + reset4: 'Аккаунт SSH по умолчанию:', + change1: 'Пожалуйста учтите, что это действие изменит следующие пароли:', + change2: 'Пароль для входа в веб-интерфейс', + change3: 'Пароль системного пользователя "root" (пароль для входа по SSH)', + change4: 'Чтобы сбросить пароли, нажмите и удерживайте кнопку BOOT на NanoKVM.' } }, wifi: { title: 'Wi-Fi', - description: 'Configure Wi-Fi for NanoKVM', - success: 'Please check the network status of NanoKVM and visit the new IP address.', - failed: 'Operation failed, please try again.', - confirmBtn: 'Ok', - finishBtn: 'Finished' + description: 'Настройка Wi-Fi для NanoKVM', + success: 'Пожалуйста, проверьте сетевой статус NanoKVM и откройте в браузере новый IP адрес.', + failed: 'Операция не удалась, пожалуйста попробуйте снова.', + confirmBtn: 'OK', + finishBtn: 'Готово' }, screen: { video: 'Видеорежим', @@ -64,13 +63,13 @@ const ru = { frameDetect: 'Экономия трафика', frameDetectTip: 'Вычисляет разницу между кадрами и прекращает передачу видеопотока, если на экране удаленного узла не обнаружено никаких изменений.', - resetHdmi: 'Reset HDMI' + resetHdmi: 'Перезагрузить HDMI подсистему' }, keyboard: { - paste: 'вставить', - tips: 'Только стандартные буквы и символы клавиатуры поддерживаются', - placeholder: 'Пожалуйста, введите', - submit: 'подтверждать', + paste: 'Вставка текста', + tips: 'Поддерживаются только стандартные буквы и символы клавиатуры', + placeholder: 'Текст для ввода', + submit: 'Вставить', virtual: 'Клавиатура', ctrlaltdel: 'Ctrl+Alt+Del' }, @@ -139,6 +138,12 @@ const ru = { input: 'Введите MAC-адрес', ok: 'ОК' }, + download: { + title: 'Скачать образ', + input: 'Введите адрес удаленного образа', + ok: 'OK', + disabled: 'Невозможно скачать образ, раздел /data находится в режиме только для чтения' + }, power: { title: 'Питание', reset: 'Экстренная перезагрузка', @@ -147,61 +152,65 @@ const ru = { powerLong: 'Питание (длинное нажатие)' }, settings: { - title: 'Settings', + title: 'Настройки', about: { title: 'О системе NanoKVM', information: 'Информация', ip: 'IP-адрес', mdns: 'Доменное имя mDNS', application: 'Версия ПО', - applicationTip: 'NanoKVM web application version', + applicationTip: 'Версия веб-программы NanoKVM', image: 'Версия прошивки', - imageTip: 'NanoKVM system image version', + imageTip: 'Версия системного образа NanoKVM', deviceKey: 'Ключ устройства', community: 'Сообщество' }, appearance: { - title: 'Appearance', - display: 'Display', - language: 'Language', - menuBar: 'Menu Bar', - menuBarDesc: 'Display icons in the menu bar' + title: 'Внешний вид', + display: 'Отображение', + language: 'Язык', + menuBar: 'Панель меню', + menuBarDesc: 'Иконки для отображения в панели меню' }, device: { - title: 'Device', + title: 'Устройство', oled: { - title: 'OLED', - description: 'OLED screen automatically sleep', - 0: 'Never', - 15: '15 sec', - 30: '30 sec', - 60: '1 min', - 180: '3 min', - 300: '5 min', - 600: '10 min', - 1800: '30 min', - 3600: '1 hour' + title: 'OLED экран', + description: 'Автоматическое отключение OLED экрана', + 0: 'Никогда', + 15: '15 сек', + 30: '30 сек', + 60: '1 мин', + 180: '3 мин', + 300: '5 мин', + 600: '10 мин', + 1800: '30 мин', + 3600: '1 час' }, wifi: { title: 'Wi-Fi', - description: 'Configure Wi-Fi', - setBtn: 'Config' + description: 'Настройка Wi-Fi', + setBtn: 'Конфигурация' }, - disk: 'Virtual Disk', - diskDesc: 'Mount virtual U-disk on the remote host', - network: 'Virtual Network', - networkDesc: 'Mount virtual network card on the remote host' + ssh: { + description: 'Включить удаленный доступ по SSH', + tip: 'Задайте сильный пароль перед включением (Аккаунт - Пароль - Обновить)' + }, + disk: 'Виртуальный диск', + diskDesc: 'Смонтировать виртуальный USB диск на удаленном хосте', + network: 'Виртуальная сеть', + networkDesc: 'Смонтировать виртуальное сетевое устройство на удаленном хосте' }, tailscale: { title: 'Tailscale', memory: { - title: 'Memory optimization', - tip: "When memory usage exceeds the limit, garbage collection is performed more aggressively to attempt to free up memory. it's recommended to set to 50MB if using Tailscale. A Tailscale restart is required for the change to take effect.", - disable: 'Disable' + title: 'Оптимизация памяти', + tip: "Когда использование ОЗУ выходит за пределы, очистка мусора происходит более агрессивно, дабы попытаться освободить память. Рекомендуется задать на 50MB при использовании Tailscale. Перезагрузка Tailscale рекомендуется для вступления данной настройки в силу.", + disable: 'Отключить' }, - restart: 'Are you sure to restart Tailscale?', - stop: 'Are you sure to stop Tailscale?', - stopDesc: 'Log out Tailscale and disable its automatic startup on boot.', + restart: 'Вы уверены что хотите перезагрузить Tailscale?', + stop: 'Вы уверены что хотите остановить Tailscale?', + stopDesc: 'Выйти из Tailscale и отключить его автоматический запуск при включении.', loading: 'Загрузка...', notInstall: 'Tailscale не найден! Пожалуйста, установите.', install: 'Установить', @@ -221,28 +230,29 @@ const ru = { enable: 'Включить Tailscale', deviceName: 'Имя устройства', deviceIP: 'IP адрес устройства', - account: 'аккаунт', + account: 'Аккаунт', logout: 'Выход', logout2: 'Вы действительно хотите выйти?', - okBtn: 'Yes', - cancelBtn: 'No' + uninstall: 'Удалить Tailscale', + okBtn: 'Да', + cancelBtn: 'Нет' }, update: { title: 'Проверить обновления', queryFailed: 'Получить версию не удалось', updateFailed: 'Обновление не удалось. Пожалуйста, попробуйте еще раз.', - isLatest: 'У вас уже есть последняя версия.', + isLatest: 'У вас уже последняя версия.', available: 'Доступно обновление. Вы уверены, что хотите обновить?', updating: 'Начато обновление. Пожалуйста, подождите...', confirm: 'Подтвердить', cancel: 'Отмена' }, account: { - title: 'Account', - webAccount: 'Web Account Name', - password: 'Password', - updateBtn: 'Update', - logoutBtn: 'Logout' + title: 'Аккаунт', + webAccount: 'Имя веб-аккаунта', + password: 'Пароль', + updateBtn: 'Обновить', + logoutBtn: 'Выйти' } } } diff --git a/web/src/i18n/locales/th.ts b/web/src/i18n/locales/th.ts new file mode 100644 index 0000000..205f5a1 --- /dev/null +++ b/web/src/i18n/locales/th.ts @@ -0,0 +1,261 @@ +const en = { + translation: { + head: { + desktop: 'Remote Desktop', + login: 'เข้าสู่ระบบ', + changePassword: 'เปลี่ยนรหัสผ่าน', + terminal: 'Terminal', + wifi: 'Wi-Fi' + }, + auth: { + login: 'เข้าสู่ระบบ', + placeholderUsername: 'ชื่อผู้ใช้งาน', + placeholderPassword: 'รห้สผ่าน', + placeholderPassword2: 'ใส่รหัสผ่านอีกครั้ง', + noEmptyUsername: 'ต้องใส่ชื่อผู้ใช้งาน', + noEmptyPassword: 'ต้องใส้รหัสผ่าน', + noAccount: 'ไม่สามารถอ่านข้อมูลได้, โปรดรีเฟรชหน้าเว็บหรือรีเซ็ตรหัสผ่าน', + invalidUser: 'ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง', + error: 'เกิดข้อผิดพลาด', + changePassword: 'เปลี่ยนรหัสผ่าน', + changePasswordDesc: 'เพื่อความปลอดภัย, โปรดเปลี่ยนรหัสผ่าน!', + differentPassword: 'รหัสผ่านไม่ตรงกัน', + illegalUsername: 'ชื่อผู้ใช้ไม่สามารถมีอักขระที่ไม่ถูกต้อง', + illegalPassword: 'รหัสผ่านไม่สามารถมีอักขระที่ไม่ถูกต้อง', + forgetPassword: 'ลืมรหัสผ่าน', + ok: 'โอเค', + cancel: 'ยกเลิก', + loginButtonText: 'เข้าสู่ระบบ', + tips: { + reset1: + 'เพื่อรีเช็ทรหัสผ่าน, กดปุ่ม BOOT ค้างไว้ 10 วินาที.', + reset2: 'สำหรับวิธีแบบลงลึก โปรดอ่าน::', + reset3: 'ค่าเริ่มต้นบัญชีเว็บ:', + reset4: 'ค่าเริ่มต้นบัญชี SSH:', + change1: 'โปรดทราบ การกระทำนี้จะเปลี่ยนรหัสดีงกล่าว:', + change2: 'รหัสผ่านเว็บไชต์', + change3: 'รหัส Root ของระบบ (ใช้เข้า SSH)', + change4: 'เพื่อรีเช็ทรหัสผ่าน, กรุณากดปุ่ม BOOT บนอุปกรณ์ NanoKVM ค้างไว้.' + } + }, + wifi: { + title: 'Wi-Fi', + description: 'ตั้งค้า Wi-Fi สำหรับ NanoKVM', + success: 'กรุณาตรวจสอบสถานะเครื่องข่ายของ NanoKVM และไปที่ IP address ใหม่', + failed: 'เกิดข้อผิดพลาด กรุณาลองใหม่.', + confirmBtn: 'โอเค', + finishBtn: 'เสร็จ' + }, + screen: { + video: 'โหมดวีดีโอ', + resolution: 'ความคมชัด', + auto: 'อัตโนมัติ', + autoTips: + "อาการภาพฉีกขาดหรือเมาส์ไม่ตรงตำแหน่งอาจเกิดขึ้นที่ความละเอียดบางระดับ แนะนำให้ปรับความละเอียดของคอมพิวเตอร์ต้นทางหรือปิดโหมดอัตโนมัติ", + fps: 'FPS', + customizeFps: 'กำหนดเอง', + quality: 'คุณภาพ', + qualityLossless: 'ไม่สูญเสียคุณภาพ', + qualityHigh: 'สูง', + qualityMedium: 'กลาง', + qualityLow: 'ต่ำ', + frameDetect: 'ตรวจจับเฟรม', + frameDetectTip: + "ระบบจะคำนวณความแตกต่างระหว่างเฟรม และหยุดส่งสตรีมวิดีโอเมื่อไม่พบการเปลี่ยนแปลงบนหน้าจอของคอมพิวเตอร์ต้นทาง", + resetHdmi: 'รีเช็ท HDMI' + }, + keyboard: { + paste: 'วาง', + tips: 'รองรับเฉพาะตัวอักษรและสัญลักษณ์มาตรฐานของแป้นพิมพ์เท่านั้น', + placeholder: 'กรุณาป้อน', + submit: 'ยืนยัน', + virtual: 'คีบอร์ด', + ctrlaltdel: 'Ctrl+Alt+Del' + }, + mouse: { + default: 'ตัวชี้เริ่มต้น', + pointer: 'ตัวชี้แบบชี้', + cell: 'ตัวชี้แบบเซลล์', + text: 'ตัวชี้แบบข้อความ', + grab: 'ตัวชี้แบบจับ', + hide: 'ซ่อนตัวชี้', + mode: 'โหมดเมาส์', + absolute: 'โหมดสัมบูรณ์', + relative: 'โหมดสัมพัทธ์', + requestPointer: 'กำลังใช้โหมดสัมพัทธ์ กรุณาคลิกที่เดสก์ท็อปเพื่อรับตัวชี้เมาส์', + resetHid: 'รีเซ็ต HID' + }, + image: { + title: 'ดิสก์จำลอง', + loading: 'กำลังโหลด...', + empty: 'ไม่พบดิสก์', + mountFailed: 'การติดตั้งล้มเหลว', + mountDesc: + "ในบางระบบ, จะต้อง eject ดิสก์จำลองก่อนที่จะใส่ดิสก์ใหม่.", + tips: { + title: 'วิธีการอัปโหลด', + usb1: 'เชื่อมต่อ NanoKVM กับคอมพิวเตอร์ของคุณผ่าน USB', + usb2: 'ตรวจสอบให้แน่ใจว่าได้เปิดใช้งานดิสก์เสมือนแล้ว (ตั้งค่า - ดิสก์เสมือน)', + usb3: 'เปิดดิสก์บนคอมพิวเตอร์ของคุณและคัดลอกไฟล์อิมเมจไปยังไดเรกทอรีหลักของดิสก์', + scp1: 'ตรวจสอบให้แน่ใจว่า NanoKVM และคอมพิวเตอร์ของคุณอยู่ในเครือข่ายเดียวกัน', + scp2: 'เปิดเทอร์มินัลบนคอมพิวเตอร์ของคุณและใช้คำสั่ง SCP เพื่ออัปโหลดไฟล์อิมเมจไปยังไดเรกทอรี /data บน NanoKVM', + scp3: 'ตัวอย่าง: scp <ที่อยู่ของไฟล์> root@:/data', + tfCard: 'การ์ด TF', + tf1: 'วิธีนี้รองรับแค่บนระบบปฏิบัติการ Linux', + tf2: 'นำการ์ด TF ออกจาก NanoKVM', + tf3: 'ใส่การ์ด TF ลงในเครื่องอ่านการ์ดและเชื่อมต่อกับคอมพิวเตอร์ของคุณ', + tf4: 'คัดลอกไฟล์อิมเมจไปยังไดเรกทอรี /data บนการ์ด TF', + tf5: 'ใส่การ์ด TF กลับเข้า NanoKVM' + } + }, + script: { + title: 'สคริปต์', + upload: 'อัปโหลด', + run: 'เรียกใช้', + runBackground: 'เรียกใช้ในพื้นหลัง', + runFailed: 'การเรียกใช้ล้มเหลว', + attention: 'ความสนใจ', + delDesc: 'คุณแน่ใจที่จะลบไฟล์นี้หรือไม่?', + confirm: 'ใช่', + cancel: 'ไม่ใช่', + delete: 'ลบ', + close: 'ปิด' + }, + terminal: { + title: 'เทอร์มินอล', + nanokvm: 'เทอร์มินอลของ NanoKVM', + serial: 'เทอร์มินอลของ Serial Port', + serialPort: 'Serial Port', + serialPortPlaceholder: 'กรุณาเลือก serial port', + baudrate: 'Baud rate', + confirm: 'ยืนยัน' + }, + wol: { + title: 'Wake-on-LAN', + sending: 'กำลังส่งคำสั่ง...', + sent: 'ส่งคำสั่งแล้ว', + input: 'กรุณาใส่ MAC Address ของเครื่องที่ต้องการปลุก', + ok: 'ยืนยัน' + }, + download: { + title: 'ดาวโหลด Disk Image', + input: 'กรุณาใส่ลิงค์ของ Disk Image.', + ok: 'ยืนยัน', + disabled: '/data มีการตั้งค่าเป็นอ่านอย่างเดียว ดังนั้นเราไม่สามารถดาวโหลด Disk Image ได้' + }, + power: { + title: 'เปิด/ปิด', + reset: 'รีเช็ท', + power: 'เปิด/ปิด', + powerShort: 'เปิด/ปิด (กดปล่อย)', + powerLong: 'เปิด/ปิด (กดค้าง)' + }, + settings: { + title: 'การตั้งค่า', + about: { + title: 'เกี๋ยวกับ NanoKVM', + information: 'ข้อมูล', + ip: 'IP', + mdns: 'mDNS', + application: 'เวอร์ชั่นแอปพิเคชั่น', + applicationTip: 'เวอร์ชั่นเว็บ NanoKVM', + image: 'เวอร์ชั่น Image ระบบ', + imageTip: 'เวอร์ชั่น Image ระบบ NanoKVM', + deviceKey: 'คีย์อุปกรณ์', + community: 'ชุมชน' + }, + appearance: { + title: 'ลักษณะการแสดงผล', + display: 'การแสดงผล', + language: 'ภาษา', + menuBar: 'แถบเมนู', + menuBarDesc: 'แสดงไอคอนในแถบเมนู' + }, + device: { + title: 'อุปกรณ์', + oled: { + title: 'OLED', + description: 'เวลาก่อนจอ OLED จะปิดอัตโนมัติ', + 0: 'ไม่เลย', + 15: '15 วินาที', + 30: '30 วินาที', + 60: '1 นาที', + 180: '3 นาทร', + 300: '5 นาที', + 600: '10 นาที', + 1800: '30 นาที', + 3600: '1 ชั่วโมง' + }, + wifi: { + title: 'Wi-Fi', + description: 'ตั้งค่า Wi-Fi', + setBtn: 'ยืนยัน' + }, + ssh: { + description: 'เปิดใช้งาน SSH', + tip: 'ตั้งรหัสผ่านทีปลอดถัยก่อนเปิดใช้งาน (บัญชี - เปลี่ยนรหัสผ่าน)' + }, + disk: 'ดิสก์จำลอง', + diskDesc: 'เปิดใช้งาน U-disk จำลอง', + network: 'เครือข่ายจำลอง', + networkDesc: 'เปิดใช้งานอุปกรณ์เครือข่ายจำลอง' + }, + tailscale: { + title: 'Tailscale', + memory: { + title: 'การปรับแต่งหน่วยความจำ', + tip: "เมื่อการใช้งานหน่วยความจำเกินขีดจำกัด การเก็บขยะจะทำงานอย่างเข้มงวดมากขึ้นเพื่อพยายามปล่อยหน่วยความจำ คำแนะนำคือการตั้งค่าเป็น 50MB หากใช้ Tailscale และต้องทำการรีสตาร์ท Tailscale เพื่อให้การเปลี่ยนแปลงมีผล", + disable: 'ปิดใช้งาน' + }, + restart: 'คุณแน่ใจที่จะรีสตาร์ท Tailscale หรือไม่?', + stop: 'คุณแน่ใจที่จะปิด Tailscale หรือไม่?', + stopDesc: 'ออกจากระบบ Tailscale และปิดใช้งานการเริ่มต้นอัตโนมัติเมื่อบูตเครื่อง', + loading: 'กำลังโหลด...', + notInstall: 'ไม่พบ Tailscale! กรุณาติดตั้ง', + install: 'ติดตั้ง', + installing: 'กำลังติดตั้ง', + failed: 'การติดตั้งล้มเหลว', + retry: 'กรุณารีเฟรชและลองใหม่ หรือพยายามติดตั้งด้วยตนเอง', + download: 'ดาวน์โหลด', + package: 'แพ็กเกจการติดตั้ง', + unzip: 'และแตกไฟล์', + upTailscale: 'อัปโหลด tailscale ไปยังไดเรกทอรี /usr/bin/ ของ NanoKVM', + upTailscaled: 'อัปโหลด tailscaled ไปยังไดเรกทอรี /usr/sbin/ ของ NanoKVM', + refresh: 'รีเฟรชหน้าปัจจุบัน', + notLogin: 'อุปกรณ์ยังไม่ได้ผูกบัญชี กรุณาล็อกอินและผูกอุปกรณ์นี้กับบัญชีของคุณ', + urlPeriod: 'ลิงก์นี้ใช้ได้ 10 นาที', + login: 'ล็อกอิน', + loginSuccess: 'ล็อกอินสำเร็จ', + enable: 'เปิดใช้งาน Tailscale', + deviceName: 'ชื่ออุปกรณ์', + deviceIP: 'IP อุปกรณ์', + account: 'บัญชีผู้ใช้', + logout: 'ออกจากระบบ', + logout2: 'คุณแน่ใจที่จะออกจากระบบหรือไม่?', + uninstall: 'ถอนการติดตั้ง Tailscale', + okBtn: 'ใช่', + cancelBtn: 'ไม่ใช่' + }, + update: { + title: 'ตรวจสอบการอัปเดต', + queryFailed: 'ไม่สามารถรับข้อมูลเวอร์ชันได้', + updateFailed: 'การอัปเดตล้มเหลว กรุณาลองใหม่', + isLatest: 'คุณมีเวอร์ชันล่าสุดแล้ว', + available: 'มีการอัปเดตใหม่ คุณแน่ใจที่จะอัปเดตหรือไม่?', + updating: 'กำลังเริ่มอัปเดต กรุณารอสักครู่...', + confirm: 'ยืนยัน', + cancel: 'ยกเลิก' + }, + account: { + title: 'บัญชี', + webAccount: 'ชื่อผู้ใช้', + password: 'รหัสผ่าน', + updateBtn: 'ยืนยัน', + logoutBtn: 'ออกจากระบบ' + } + } + } + }; + + export default en; + \ No newline at end of file diff --git a/web/src/i18n/locales/zh.ts b/web/src/i18n/locales/zh.ts index 55b7e68..57efb88 100644 --- a/web/src/i18n/locales/zh.ts +++ b/web/src/i18n/locales/zh.ts @@ -87,6 +87,7 @@ const zh = { title: '镜像', loading: '加载中', empty: '无镜像文件', + cdrom: '以 CD-ROM 模式挂载镜像', mountFailed: '挂载失败', mountDesc: '在某些系统中,需要在远程主机中弹出虚拟硬盘后再挂载镜像。', tips: { @@ -134,6 +135,12 @@ const zh = { input: '请输入MAC地址', ok: '确定' }, + download: { + title: '下载镜像', + input: '请输入镜像的下载地址', + ok: '确定', + disabled: '/data 是只读分区,无法下载镜像' + }, power: { title: '电源', reset: '重启', @@ -182,6 +189,10 @@ const zh = { description: '配置 Wi-Fi 信息', setBtn: '设置' }, + ssh: { + description: '启用 SSH 远程访问', + tip: '启用前请务必设置强密码(帐号 - 修改密码)' + }, disk: '虚拟U盘', diskDesc: '在远程主机中挂载虚拟U盘', network: '虚拟网卡', @@ -219,6 +230,7 @@ const zh = { account: '账号', logout: '退出', logout2: '确认退出?', + uninstall: '卸载 Tailscale', okBtn: '确认', cancelBtn: '取消' }, diff --git a/web/src/i18n/locales/zh_tw.ts b/web/src/i18n/locales/zh_tw.ts index becbcd1..91fe8cf 100644 --- a/web/src/i18n/locales/zh_tw.ts +++ b/web/src/i18n/locales/zh_tw.ts @@ -134,6 +134,12 @@ const zh_tw = { input: '請輸入 MAC 位址', ok: '確定' }, + download: { + title: '下载映像', + input: '請輸入映像檔的下載URL', + ok: '確定', + disabled: '/data 為唯讀目錄,無法下載映像檔' + }, power: { title: '電源', reset: '重新啟動', @@ -182,6 +188,10 @@ const zh_tw = { description: '設定 Wi-Fi', setBtn: '設定' }, + ssh: { + description: '啟用SSH', + tip: '啟用前請務必設定強密碼(帳號 - 更改密碼)' + }, disk: '虛擬隨身碟', diskDesc: '在遠端主機上連接虛擬隨身碟', network: '虛擬網卡', @@ -219,6 +229,7 @@ const zh_tw = { account: '帳號', logout: '登出', logout2: '確認登出?', + uninstall: '移除 Tailscale', okBtn: '確認', cancelBtn: '取消' }, diff --git a/web/src/lib/encrypt.ts b/web/src/lib/encrypt.ts index 7b62f8c..912473e 100644 --- a/web/src/lib/encrypt.ts +++ b/web/src/lib/encrypt.ts @@ -1,14 +1,9 @@ import CryptoJS from 'crypto-js'; +// This key is only used to prevent the data from being transmitted in plaintext. const SECRET_KEY = 'nanokvm-sipeed-2024'; export function encrypt(data: string) { const dataEncrypt = CryptoJS.AES.encrypt(data, SECRET_KEY).toString(); return encodeURIComponent(dataEncrypt); } - -export function decrypt(data: string) { - const bytes = CryptoJS.AES.decrypt(data, SECRET_KEY); - const dataDecrypt = bytes.toString(CryptoJS.enc.Utf8); - return decodeURIComponent(dataDecrypt); -} diff --git a/web/src/pages/auth/login/index.tsx b/web/src/pages/auth/login/index.tsx index d27d561..e3afdd8 100644 --- a/web/src/pages/auth/login/index.tsx +++ b/web/src/pages/auth/login/index.tsx @@ -14,6 +14,8 @@ import { Tips } from './tips.tsx'; export const Login = () => { const navigate = useNavigate(); const { t } = useTranslation(); + + const [isLoading, setIsloading] = useState(false); const [msg, setMsg] = useState(''); useEffect(() => { @@ -29,6 +31,9 @@ export const Login = () => { }, [msg]); function login(values: any) { + if (isLoading) return; + setIsloading(true); + const username = values.username; const password = encrypt(values.password); @@ -36,14 +41,7 @@ export const Login = () => { .login(username, password) .then((rsp: any) => { if (rsp.code !== 0) { - if (rsp.code === -3) { - setMsg(t('auth.noAccount')); - } else if (rsp.code === -4) { - setMsg(t('auth.invalidUser')); - } else { - setMsg(t('auth.error')); - } - + setMsg(rsp.code === -2 ? t('auth.invalidUser') : t('auth.error')); return; } @@ -55,6 +53,9 @@ export const Login = () => { }) .catch(() => { setMsg(t('auth.error')); + }) + .finally(() => { + setIsloading(false); }); } @@ -63,13 +64,12 @@ export const Login = () => {
-
-

{t('auth.login')}

+

{t('auth.login')}

{
{msg}
- diff --git a/web/src/pages/desktop/menu/download.tsx b/web/src/pages/desktop/menu/download.tsx index dab973d..194000b 100644 --- a/web/src/pages/desktop/menu/download.tsx +++ b/web/src/pages/desktop/menu/download.tsx @@ -6,7 +6,7 @@ import { useSetAtom } from 'jotai'; import { DownloadIcon } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { downloadImage, statusImage, imageEnabled } from '@/api/download.ts'; +import { downloadImage, imageEnabled, statusImage } from '@/api/download.ts'; import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts'; export const DownloadImage = () => { @@ -21,12 +21,10 @@ export const DownloadImage = () => { const [diskEnabled, setDiskEnabled] = useState(false); const [popoverKey, setPopoverKey] = useState(0); - const inputRef = useRef(null); const intervalId = useRef(undefined); - useEffect(() => { checkDiskEnabled(); }, []); @@ -34,7 +32,6 @@ export const DownloadImage = () => { function checkDiskEnabled() { imageEnabled() .then((res) => { - console.log(res.data.enabled); setDiskEnabled(res.data.enabled); }) .catch(() => { @@ -48,9 +45,9 @@ export const DownloadImage = () => { getDownloadStatus(); if (!intervalId.current) { intervalId.current = setInterval(getDownloadStatus, 2500); - }; + } setIsKeyboardEnable(false); - setPopoverKey(prevKey => prevKey + 1); // Force re-render + setPopoverKey((prevKey) => prevKey + 1); // Force re-render } else { setInput(''); setStatus(''); @@ -75,45 +72,50 @@ export const DownloadImage = () => { if (rsp.data.status === 'in_progress') { // Check if rsp has a percentage value if (rsp.data.percentage) { - setLog('Downloading ('+ rsp.data.percentage + ')' + ': ' + rsp.data.file); + setLog('Downloading (' + rsp.data.percentage + ')' + ': ' + rsp.data.file); } else { setLog('Downloading' + ': ' + rsp.data.file); } setInput(rsp.data.file); - }; + } if (rsp.data.status === 'failed') { setLog('Failed'); clearInterval(intervalId.current); - }; + } if (rsp.data.status === 'idle') { - setLog(''); // Clear the log - clearInterval(intervalId.current); - }; - }; + setLog(''); // Clear the log + clearInterval(intervalId.current); + } + } }); - }; + } function download(url?: string) { + if (!url) return; + setStatus('in_progress'); setLog('Downloading: ' + url); // start the getDownloadStatus to tick every 5 seconds - downloadImage(url).then(() => { - getDownloadStatus(); - // Start the interval to check the download status - if (!intervalId.current) { - intervalId.current = setInterval(getDownloadStatus, 2500);} - }).catch(() => { - clearInterval(intervalId.current); // Clear the interval when the download is complete or fails - setStatus('failed'); - setLog('Failed'); - }); + downloadImage(url) + .then(() => { + getDownloadStatus(); + // Start the interval to check the download status + if (!intervalId.current) { + intervalId.current = setInterval(getDownloadStatus, 2500); + } + }) + .catch(() => { + clearInterval(intervalId.current); // Clear the interval when the download is complete or fails + setStatus('failed'); + setLog('Failed'); + }); } const content = (
- {t('download.download')} + {t('download.title')}
@@ -124,8 +126,17 @@ export const DownloadImage = () => { <>
{t('download.input')}
- -
@@ -152,10 +163,11 @@ export const DownloadImage = () => { content={content} placement="bottomLeft" trigger="click" + arrow={false} open={isPopoverOpen} onOpenChange={handleOpenChange} > -
+
diff --git a/web/src/pages/desktop/menu/image/images.tsx b/web/src/pages/desktop/menu/image/images.tsx index c21a7a4..c68ff45 100644 --- a/web/src/pages/desktop/menu/image/images.tsx +++ b/web/src/pages/desktop/menu/image/images.tsx @@ -14,10 +14,11 @@ import * as api from '@/api/storage.ts'; import { client } from '@/lib/websocket.ts'; type ImagesProps = { + cdrom: boolean; setIsMounted: (isMounted: boolean) => void; }; -export const Images = ({ setIsMounted }: ImagesProps) => { +export const Images = ({ cdrom, setIsMounted }: ImagesProps) => { const { t } = useTranslation(); const [notify, contextHolder] = notification.useNotification(); @@ -77,7 +78,7 @@ export const Images = ({ setIsMounted }: ImagesProps) => { const filename = mountedImage === image ? '' : image; api - .mountImage(filename) + .mountImage(filename, cdrom) .then((rsp) => { if (rsp.code !== 0) { openNotification(); diff --git a/web/src/pages/desktop/menu/image/index.tsx b/web/src/pages/desktop/menu/image/index.tsx index b0e5d29..d93da94 100644 --- a/web/src/pages/desktop/menu/image/index.tsx +++ b/web/src/pages/desktop/menu/image/index.tsx @@ -1,11 +1,11 @@ import { useEffect, useState } from 'react'; -import { Divider, Popover } from 'antd'; +import { Divider, Popover, Switch, Tooltip } from 'antd'; import clsx from 'clsx'; import { DiscIcon } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useMediaQuery } from 'react-responsive'; -import { getMountedImage } from '@/api/storage.ts'; +import { getCdRom, getMountedImage } from '@/api/storage.ts'; import { Images } from './images.tsx'; import { Tips } from './tips.tsx'; @@ -16,24 +16,40 @@ export const Image = () => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isMounted, setIsMounted] = useState(false); + const [cdrom, setCdrom] = useState(false); useEffect(() => { getMountedImage().then((rsp) => { if (rsp.code !== 0) return; setIsMounted(!!rsp.data?.file); }); + + getCdRom().then((rsp) => { + if (rsp.code !== 0) return; + setCdrom(rsp.data?.cdrom === 1); + }); }, []); const content = (
- {t('image.title')} - +
+ {t('image.title')} + +
+ + +
+ CD-ROM + + setCdrom(checked)}> +
+
- {isPopoverOpen && } + {isPopoverOpen && }
); diff --git a/web/src/pages/desktop/menu/image/tips.tsx b/web/src/pages/desktop/menu/image/tips.tsx index 01af38f..516fe0e 100644 --- a/web/src/pages/desktop/menu/image/tips.tsx +++ b/web/src/pages/desktop/menu/image/tips.tsx @@ -51,13 +51,13 @@ export const Tips = () => { return ( <>
setIsModalOpen(true)} > - - {t('image.tips.title')} - + {/**/} + {/* {t('image.tips.title')}*/} + {/**/}
{ @@ -48,12 +48,12 @@ export const Menu = () => { {!menuDisabledItems.includes('image') && } + {!menuDisabledItems.includes('download') && } {!menuDisabledItems.includes('script') &&