超碰人人人人人,亚洲AV午夜福利精品一区二区,亚洲欧美综合区丁香五月1区,日韩欧美亚洲系列

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

用.NET IoT庫控制舵機(jī)并多方法播放表情

freeflydom
2025年2月6日 10:26 本文熱度 39

前言

前面兩篇文章講了.NET IoT相關(guān)的知識點(diǎn),以及硬件的GPIO的一些概念,還有點(diǎn)亮兩個(gè)屏幕的方法,這些讓大家對.NET的用途有了新的認(rèn)識,那我們這回繼續(xù)講解.NET IoT的知識點(diǎn),以及介紹一些好玩的東西,例如讓視頻通過機(jī)器人的屏幕播放起來,還有機(jī)器人的身體也能通過我們的代碼控制動起來。大家感興趣的話可以跟著我的文章繼續(xù)下去,另外說下我B站更新了機(jī)器人相關(guān)視頻,所以大家可以跟著觀看制作,視頻包含了機(jī)器人的組裝和打印文件使用,點(diǎn)擊圖片即可跳轉(zhuǎn)。

問題解答

大家看完這篇文章,大概對機(jī)器人的一些功能模塊有了了解,大家肯定會有疑問,做這個(gè)機(jī)器人到底需要什么電路板,以及只用樹莓派到底能夠做到什么程度,我會挑一些大家可能會問的問題做一些解答。

1. 只用樹莓派可以控制舵機(jī)嗎?

只用樹莓派控制舵機(jī)是OK,舵機(jī)本身是使用PWM的信號進(jìn)行控制的,這個(gè)可以通過樹莓派的引腳進(jìn)行模擬,這個(gè)不在本文章的討論范圍內(nèi),有需要可以單獨(dú)寫一篇文章進(jìn)行講解。

2. 機(jī)器人的制作到底需要哪些電路板?

下圖為完整的硬件相關(guān)的部分,大家可以大概的了解到機(jī)器人的電路構(gòu)成。

目前機(jī)器人總共需要三塊板子,一塊是我設(shè)計(jì)的搭配樹莓派使用的,另外兩塊是使用的ElectronBot精英版A2的一個(gè)舵機(jī)驅(qū)動板(用來改裝舵機(jī)并且驅(qū)動舵機(jī)的運(yùn)動),一個(gè)語音板子(包含麥克風(fēng),喇叭,和攝像頭連接),這些大家都可以通過在閑魚之類搜索ElectronBot相關(guān)的關(guān)鍵詞買到,大家不要懼怕自己不會焊接電路板不能學(xué)習(xí)之類。即使大家買不到電路板,通過文章進(jìn)行學(xué)習(xí)也是問題不大的,所以大家不要擔(dān)心。

3. 如果想學(xué)習(xí)應(yīng)該怎么樣獲得電路板?

這個(gè)現(xiàn)在網(wǎng)絡(luò)上都有一站式創(chuàng)客電路板生產(chǎn)的平臺,例如嘉立創(chuàng)(這個(gè)非廣告因?yàn)檫@個(gè)是國內(nèi)算是很成熟的平臺了),我剛才提到的ElectronBot精英版A2和我的樹莓派拓展板子都在立創(chuàng)的開源廣場有提供,大家直接跟著下單就能夠拿到電路板了,然后就可以購買芯片物料焊接了。

4. ElectronBot和我做的機(jī)器人有什么關(guān)系?

ElectronBot是稚暉君(B站一個(gè)有名的UP主)制作的一個(gè)開源的必須連接電腦的桌面機(jī)器人,我和網(wǎng)友在他的方案基礎(chǔ)上優(yōu)化了電路板出了一個(gè)ElectronBot精英版A2的版本,現(xiàn)在我通過用樹莓派替換了ElectronBot的屏幕控制和舵機(jī)控制部分,實(shí)現(xiàn)了一個(gè)獨(dú)立的版本,我為了省事,就借用了ElectronBot的兩個(gè)電路板,省的自己設(shè)計(jì)了。

名詞解釋

1. 什么是舵機(jī)?

舵機(jī)是一種位置(角度)伺服的驅(qū)動器,適用于那些需要角度不斷變化并可以保持的控制系統(tǒng)。舵機(jī)通過一瞬間的堵轉(zhuǎn)扭力將舵盤進(jìn)行轉(zhuǎn)向,持續(xù)的時(shí)間短。最早以前在高檔遙控玩具,如飛機(jī)、潛艇模型,遙控機(jī)器人中已經(jīng)得到了普遍應(yīng)用。如今通過技術(shù)的革新,加工工藝的升級,舵機(jī)在各行各業(yè)中已得到越來越廣泛的應(yīng)用。

2. 什么是I2C通訊?

I2C(Inter-Integrated Circuit),讀作:I方C,是一種同步、多主多從架構(gòu)、雙向雙線的串行通信總線,通常應(yīng)用于短距離、低速通信場景,廣泛用于微控制器和各種外圍設(shè)備之間的通信。它使用兩條線路:串行數(shù)據(jù)線(SDA)和串行時(shí)鐘線(SCL)進(jìn)行雙向傳輸。

3. 什么是lottie動畫?

Lottie 是一種輕量級的基于 JSON 的動畫格式,可以在任何設(shè)備或?yàn)g覽器上播放。設(shè)計(jì)師和開發(fā)人員廣泛使用它來改善網(wǎng)站和應(yīng)用程序的交互。Lottie 的矢量結(jié)構(gòu)允許用戶在不失去圖像質(zhì)量或增加文件大小的情況下縮放動畫。

4. 什么是ffmpeg?

FFmpeg 是一個(gè)完整的跨平臺音視頻解決方案,用于記錄、轉(zhuǎn)換和流式處理音視頻。它是目前最強(qiáng)大的音視頻處理開源軟件之一,被廣泛應(yīng)用于視頻網(wǎng)站、播放器、編碼器等多種場景中。

舵機(jī)控制

舵機(jī)控制板固件相關(guān)介紹

  1. 首先我們象征性的看下舵機(jī)板子的固件代碼,舵機(jī)控制板使用STM32F103標(biāo)準(zhǔn)庫硬件IIC+DMA的類似方案進(jìn)行數(shù)據(jù)讀寫,有社區(qū)的人進(jìn)行了優(yōu)化,但是核心代碼大體相同,改裝的舵機(jī)板比舵機(jī)原始的只支持角度控制有更多的玩法。參考如下文檔:
    STM32F103標(biāo)準(zhǔn)庫硬件IIC+DMA連續(xù)數(shù)據(jù)發(fā)送、接收
    ,核心代碼如下:
    // // Command handler
    void I2C_SlaveDMARxCpltCallback()
    {
        ErrorStatus state;
        float valF = *((float*) (i2cDataRx + 1));
        i2cDataTx[0] = i2cDataRx[0];
        switch (i2cDataRx[0])
        {
            case 0x01:  // Set angle
            {
                motor.dce.setPointPos = valF;
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x02: // Set velocity
            {
                motor.dce.setPointVel = valF;
                auto* b = (unsigned char*) &(motor.velocity);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x03: // Set torque
            {
                motor.SetTorqueLimit(valF);
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x11: // Get angle
            {
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x12: // Get velocity
            {
                auto* b = (unsigned char*) &(motor.velocity);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x21: // Set id
            {
                boardConfig.nodeId = i2cDataRx[1];
                boardConfig.configStatus = CONFIG_COMMIT;
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x22: // Set kp
            {
                motor.dce.kp = valF;
                boardConfig.dceKp = valF;
                boardConfig.configStatus = CONFIG_COMMIT;
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x23: // Set ki
            {
                motor.dce.ki = valF;
                boardConfig.dceKi = valF;
                boardConfig.configStatus = CONFIG_COMMIT;
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x24: // Set kv
            {
                motor.dce.kv = valF;
                boardConfig.dceKv = valF;
                boardConfig.configStatus = CONFIG_COMMIT;
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x25: // Set kd
            {
                motor.dce.kd = valF;
                boardConfig.dceKd = valF;
                boardConfig.configStatus = CONFIG_COMMIT;
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x26: // Set torque limit
            {
                motor.SetTorqueLimit(valF);
                boardConfig.toqueLimit = valF;
                boardConfig.configStatus = CONFIG_COMMIT;
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x27: // Set init pos
            {
                boardConfig.initPos = valF;
                boardConfig.configStatus = CONFIG_COMMIT;
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0xff:
                motor.SetEnable(i2cDataRx[1] != 0);
                break;
            default:
                break;
        }
        do
        {
        state = Slave_Transmit(i2cDataTx,5,5000);
        } while (state != SUCCESS);
        if(i2cDataRx[0] == 0x21)
        {
            Set_ID(boardConfig.nodeId);
        }
    }
    // Control loop
    void TIM14_PeriodElapsedCallback()
    {
            // Read sensor data
        LL_DMA_EnableChannel(DMA1,LL_DMA_CHANNEL_1);  
        LL_ADC_REG_StartConversion(ADC1);
        
        motor.angle = motor.mechanicalAngleMin +
                        (motor.mechanicalAngleMax - motor.mechanicalAngleMin) *
                        ((float) adcData[0] - (float) motor.adcValAtAngleMin) /
                        ((float) motor.adcValAtAngleMax - (float) motor.adcValAtAngleMin);
        // Calculate PID
        motor.CalcDceOutput(motor.angle, 0);
        motor.SetPwm((int16_t) motor.dce.output);
    }
    
  2. 固件控制指令對照圖,這些指令是通過樹莓派I2C引腳進(jìn)行發(fā)送。
  3. 個(gè)人的一些心得,控制板核心邏輯有個(gè)死循環(huán),如果通訊不正常,會一直等待,所以如果樹莓派的執(zhí)行控制代碼發(fā)送的不對,會出現(xiàn)I2C引腳超時(shí)的錯(cuò)誤,這個(gè)大家操作的時(shí)候一定要記住接線是否正確,代碼是否配置OK。
  4. I2C設(shè)備都是并聯(lián)到I2C總線上的,每個(gè)設(shè)備都有一個(gè)設(shè)備的ID,所以我們在和設(shè)備通訊的時(shí)候一定要指定設(shè)備的ID才能完成初始化。

舵機(jī)控制代碼編寫

由于我做的獨(dú)立版桌面機(jī)器人目前只用到了兩個(gè)舵機(jī),所以我選擇了2號和3號ID的舵機(jī)進(jìn)行控制。通過初始化I2C設(shè)備對象,進(jìn)行通訊的建立,并進(jìn)行角度的控制。示例代碼是將舵機(jī)循環(huán)往復(fù)的運(yùn)動180°,使用.NET IoT庫編寫,并在樹莓派上部署使用,示例代碼如下:

using System.Device.I2c;
try
{
    while (true)
    {
        using I2cDevice i2c = I2cDevice.Create(new I2cConnectionSettings(1, 0x02));
        using I2cDevice i2c8 = I2cDevice.Create(new I2cConnectionSettings(1, 0x03));
        byte[] writeBuffer = new byte[5] { 0xff, 0x01, 0x00, 0x00, 0x00 };
        byte[] receiveData = new byte[5];
        i2c.WriteRead(writeBuffer, receiveData);
        i2c8.WriteRead(writeBuffer, receiveData);
        for (int i = 0; i < 180; i += 1)
        {
            float angle = i;
            byte[] angleBytes = BitConverter.GetBytes(angle);
            writeBuffer[0] = 0x01;
            Array.Copy(angleBytes, 0, writeBuffer, 1, angleBytes.Length);
            i2c.WriteRead(writeBuffer, receiveData);
            i2c8.WriteRead(writeBuffer, receiveData);
            Thread.Sleep(20);
        }
        for (int i = 180; i > 0; i -= 1)
        {
            float angle = i;
            byte[] angleBytes = BitConverter.GetBytes(angle);
            writeBuffer[0] = 0x01;
            Array.Copy(angleBytes, 0, writeBuffer, 1, angleBytes.Length);
           i2c.WriteRead(writeBuffer, receiveData);
            i2c8.WriteRead(writeBuffer, receiveData);
            Thread.Sleep(20);
        }
        Console.WriteLine($"I2C 2 8 設(shè)備連接成功--{DateTime.Now.ToString("s")}");
        foreach (var data in receiveData)
        {
            Console.Write($"{data}, ");
        }
        //Console.WriteLine();
        //Thread.Sleep(500);      
    }
}
catch (Exception ex)
{
    Console.WriteLine($"I2C 設(shè)備連接失敗: {ex.Message}");
}
Console.ReadLine();

控制代碼看起來很簡單,但是這里有個(gè)坑,就是大家也看到了一個(gè)奇怪的地方,就是為什么發(fā)送數(shù)據(jù)的時(shí)候要用WriteRead這個(gè)方法,而不是先write再Read這樣的操作。其實(shí)這里也卡住我了,我翻了固件的源碼,我懷疑是因?yàn)槎鏅C(jī)版子的速度太快了,導(dǎo)致讀寫的區(qū)分不大,如果我只是寫入數(shù)據(jù)再讀取會導(dǎo)致循環(huán)卡住,這里我是推測,我翻了.NET IoT的這個(gè)I2C通訊的源碼,然后我用了WriteRead這個(gè)方法測試,發(fā)現(xiàn)通訊是OK的,如果有大佬能給出更詳細(xì)的解答,歡迎評論區(qū)給大家科普一下。到這里舵機(jī)的控制就算是完成了,具體更詳細(xì)的控制大家可以根據(jù)控制指令手冊進(jìn)行編寫測試。

舵機(jī)測試

下圖標(biāo)出了樹莓派的I2C引腳位置,這兩個(gè)引腳和舵機(jī)控制板的I2C引腳進(jìn)行接線就可以通訊了,舵機(jī)板子需要供電,而且舵機(jī)板子的地線要和樹莓派板子共地,如果是其他的I2C設(shè)備也是一樣,例如陀螺儀,I2C屏幕。

如果接線OK,代碼運(yùn)行OK,正常情況下會看到舵機(jī)旋轉(zhuǎn)的樣子。

看到這里大家有什么疑問可以在評論區(qū)討論。

多種方式播放表情

這篇文章的篇幅有點(diǎn)長,上面我們講了舵機(jī)的控制,上一篇文章我們調(diào)通了屏幕的顯示,但是只顯示圖片其實(shí)不夠生動的,如果我們能夠配上表情的播放那就生動多了。

解析lottie動畫文件進(jìn)行播放

上面的名詞解釋我們解釋了什么是lottie動畫,那我們就直接看代碼吧,這個(gè)lottie動畫目前我在樹莓派上進(jìn)行解析不是很流暢,所以只是作為知識講解,大家如果是樹莓派4或者5應(yīng)該性能很好,解析起來應(yīng)該不費(fèi)勁,而且如果代碼能夠優(yōu)化一些應(yīng)該也可以流暢。

我的做法是通過使用一些解析庫,能夠解析lottie動畫,提取出幀數(shù)據(jù),然后解析成ImageSharp的Image類,然后轉(zhuǎn)換成字節(jié)數(shù)組就可以進(jìn)行播放了。下面是我找到的社區(qū)的一些開源庫,SkiaSharp.Skottie有提供解析功能。

	<ItemGroup>
		<PackageReference Include="SkiaSharp" Version="3.116.1" />
		<PackageReference Include="SkiaSharp.Skottie" Version="3.116.1" />
		<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
	</ItemGroup>

核心的解析動畫并轉(zhuǎn)成Image的代碼如下:

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SkiaSharp;
using SkiaSharp.Skottie;
namespace Verdure.LottieToImage;
public class LottieToImage
{
    public static Image<Bgra32> RenderLottieFrame(Animation animation, double progress, int width, int height)
    {
        // 創(chuàng)建SKSurface用于渲染
        using var bitmap = new SKBitmap(width, height);
        using var canvas = new SKCanvas(bitmap);
        // 清除背景
        canvas.Clear(SKColors.Transparent);
        animation.SeekFrameTime(progress);
        animation.Render(canvas, new SKRect(0, 0, width, height));
        // 將SKBitmap轉(zhuǎn)換為byte數(shù)組
        using var image = SKImage.FromBitmap(bitmap);
        using var data = image.Encode(SKEncodedImageFormat.Png, 100);
        var bytes = data.ToArray();
        // 轉(zhuǎn)換為ImageSharp格式
        using var memStream = new MemoryStream(bytes);
        return Image.Load<Bgra32>(memStream);
    }
    public static async Task SaveLottieFramesAsync(string lottieJsonPath, string outputDir, int width, int height)
    {
        Directory.CreateDirectory(outputDir);
        // 讀取Lottie JSON文件
        var animation = Animation.Create(lottieJsonPath);
        if (animation != null)
        {
            //幀數(shù)
            var frameCount = animation.OutPoint;
            for (int i = 0; i < frameCount; i++)
            {
                var progress = animation.Duration.TotalSeconds / (frameCount - i);
                var frame = RenderLottieFrame(animation, progress, width, height);
                await frame.SaveAsPngAsync(Path.Combine(outputDir, $"frame_{i:D4}.png"));
            }
        }
    }
}

轉(zhuǎn)成Image對象之后,就可以使用我們上一篇文章里的方法轉(zhuǎn)成字節(jié)數(shù)組寫入到屏幕了。這個(gè)大家有興趣可以查看我的項(xiàng)目代碼里,有做demo測試。

桌面桌面機(jī)器人倉庫地址

通過轉(zhuǎn)換MP4格式文件進(jìn)行播放

這一種方式我是事先通過ffmpeg解析mp4的表情文件,然后將表情轉(zhuǎn)換成屏幕直接顯示的字節(jié)數(shù)組,并且序列化到j(luò)son文件里,這樣將解析轉(zhuǎn)換的部分的邏輯前置處理了,樹莓派在播放表情的時(shí)候就可以很輕松了。
核心轉(zhuǎn)換代碼邏輯如下:

將視頻幀轉(zhuǎn)成圖片的字節(jié)數(shù)組代碼:

using FFmpeg.NET;
using FFmpegImageSharp.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace FFmpegImageSharp.Services;
public class FrameExtractor
{
    public async Task<List<FrameData>> ExtractFramesAsync(string filePath)
    {
        var frames = new List<FrameData>();
        var ffmpeg = new Engine("C:\\ffmpeg-n7.1-latest-win64-gpl-7.1\\ffmpeg-n7.1-latest-win64-gpl-7.1\\bin\\ffmpeg.exe"); // Specify the path to ffmpeg executable
        var mediaFile = new InputFile(filePath); // Use a concrete class instead of MediaFile
        var mediaInfo = await ffmpeg.GetMetaDataAsync(mediaFile, CancellationToken.None);
        var duration = mediaInfo.Duration;
        var frameRate = mediaInfo.VideoData.Fps;
        var frameCount = (int)(duration.TotalSeconds * frameRate);
        for (var i = 0; i < frameCount; i++)
        {
            var timestamp = TimeSpan.FromSeconds(i / frameRate);
            var outputFilePath = $"frame_{i}.jpg";
            var arguments = $"-i \"{filePath}\" -vf \"select='eq(n\\,{i})'\" -vsync vfr -q:v 2 \"{outputFilePath}\"";
            await ffmpeg.ExecuteAsync(arguments, CancellationToken.None);
            var frameImage = await File.ReadAllBytesAsync(outputFilePath);
            var frameData = new FrameData
            {
                ImageData = frameImage,
                Timestamp = timestamp
            };
            frames.Add(frameData);
        }
        return frames;
    }
}

將圖片字節(jié)數(shù)組轉(zhuǎn)成顯示屏需要的字節(jié)數(shù)組數(shù)據(jù)的代碼如下:

using FFmpegImageSharp.Models;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace FFmpegImageSharp.Services;
public class ImageProcessor
{
    public byte[] ProcessImage(FrameData frame)
    {
        using (var image = Image.Load(frame.ImageData))
        {
            // Resize the image to 240x240
            image.Mutate(x => x.Resize(240, 240));
            // Create a new 320x240 image with a custom background color
            using (var background = new Image<Bgra32>(320, 240, new Bgra32(0, 0, 0))) // Custom color: black
            {
                // Calculate the position to center the 240x240 image on the 320x240 background
                var x = (background.Width - image.Width) / 2;
                var y = (background.Height - image.Height) / 2;
                // Draw the resized image onto the background
                background.Mutate(ctx => ctx.DrawImage(image, new Point(x, y), 1f));
                background.Mutate(x => x.Rotate(90));
                using Image<Bgr24> converted2inch4Image = background.CloneAs<Bgr24>();
                var byteList = GetImageBytes(converted2inch4Image);
                return byteList;
                // Save the processed image or perform further processing
                //background.Save($"path_to_save_processed_image_{DateTime.Now.Ticks}.png");
            }
        }
    }
    public byte[] GetImageBytes(Image<Bgr24> image, int xStart = 0, int yStart = 0)
    {
        int imwidth = image.Width;
        int imheight = image.Height;
        var pix = new byte[imheight * imwidth * 2];
        for (int y = 0; y < imheight; y++)
        {
            for (int x = 0; x < imwidth; x++)
            {
                var color = image[x, y];
                pix[(y * imwidth + x) * 2] = (byte)((color.R & 0xF8) | (color.G >> 5));
                pix[(y * imwidth + x) * 2 + 1] = (byte)(((color.G << 3) & 0xE0) | (color.B >> 3));
            }
        }
        return pix;
    }
}

主程序序列化表情到j(luò)son數(shù)據(jù)的代碼如下:

using System.Text.Json;
using FFmpegImageSharp.Models;
using FFmpegImageSharp.Services;
using Microsoft.Extensions.DependencyInjection;
var serviceProvider = new ServiceCollection()
            .AddSingleton<FrameExtractor>()
            .AddSingleton<StreamFrameExtractor>()
            .AddSingleton<ImageProcessor>()
            .BuildServiceProvider();
var frameExtractor = serviceProvider.GetRequiredService<FrameExtractor>();
//var streamFrameExtractor = serviceProvider.GetRequiredService<StreamFrameExtractor>();
var imageProcessor = serviceProvider.GetRequiredService<ImageProcessor>();
var videoFilePath = "anger.mp4"; // Update with your video file path
var data = new FrameMetaData
{
    Name = Path.GetFileNameWithoutExtension(videoFilePath),
    FileName = videoFilePath,
    Width = 240,
    Height = 320
};
var frames = await frameExtractor.ExtractFramesAsync(videoFilePath);
foreach (var frame in frames)
{
    var list = imageProcessor.ProcessImage(frame);
    data.FrameDatas.Add(list);
}
// JSON serialization
await File.WriteAllTextAsync($"{data.Name}.json", JsonSerializer.Serialize(data));
// JSON deserialization
var deserializedData = JsonSerializer.Deserialize<FrameMetaData>(await File.ReadAllTextAsync($"{data.Name}.json"));
// Verify deserialization
Console.WriteLine($"Name: {deserializedData?.Name}, Width: {deserializedData?.Width}, Height: {deserializedData?.Height}");
Console.WriteLine("Frame extraction and processing completed. Metadata saved to frame_metadata.json.");

通過上面的代碼就可以制作出一個(gè)表情文件了,源代碼在我另外的倉庫里,然后通過桌面機(jī)器人倉庫的代碼反序列化并且播放就好了。效果如下:

總結(jié)感悟

轉(zhuǎn)自https://www.cnblogs.com/GreenShade/p/18692804


該文章在 2025/2/6 10:26:44 編輯過
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點(diǎn)晴ERP是一款針對中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國內(nèi)大量中小企業(yè)的青睞。
點(diǎn)晴PMS碼頭管理系統(tǒng)主要針對港口碼頭集裝箱與散貨日常運(yùn)作、調(diào)度、堆場、車隊(duì)、財(cái)務(wù)費(fèi)用、相關(guān)報(bào)表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點(diǎn),圍繞調(diào)度、堆場作業(yè)而開發(fā)的。集技術(shù)的先進(jìn)性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點(diǎn)晴WMS倉儲管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質(zhì)期管理,貨位管理,庫位管理,生產(chǎn)管理,WMS管理系統(tǒng),標(biāo)簽打印,條形碼,二維碼管理,批號管理軟件。
點(diǎn)晴免費(fèi)OA是一款軟件和通用服務(wù)都免費(fèi),不限功能、不限時(shí)間、不限用戶的免費(fèi)OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved