视频转帧图片工具:半天写的一个小东西

用 Python + OpenCV 搭了个 GUI 桌面工具,能批量导视频、支持三种抽帧方式,多线程并行写入。主要是为了给 YOLO训练准备数据用。

为什么写这个?

最近在折腾 YOLO 目标检测模型的训练。做过的人都知道,调参是一回事,数据集要是拉胯,什么算法都救不回来。

我的原始数据是一堆录好的视频,需要先把它们拆成一帧一帧的图片再标注。寻思着自己去找一个工具,不如按照自己的需求来开发一下。

所以花了半天时间,自己写了一个。用下来还不错,整理一下发出来。

能做什么?

功能说明
批量导入支持多选视频文件,也可以直接导入整个文件夹(自动递归扫描)
三种提取模式按帧率 / 按时长间隔 / 全帧导出
双格式输出JPG(质量可调 50-100)和 PNG
自定义命名支持变量占位符拼文件名
多线程写入1-16 线程可调,视 CPU 情况而定
实时进度进度条 + 处理速度 + 日志,卡在哪一目了然
兼容格式mp4 / avi / mkv / mov / wmv / flv / webm / m4v / ts / mpg / mpeg 都试过

界面长什么样?

打开之后界面分三个区域,从上往下:

  1. 视频文件列表区——添加、移除、管理视频
  2. 参数设置区——提取模式、输出格式、命名规则、输出目录
  3. 转换进度区——进度条、状态、速度、日志

布局没什么花样,就是堆叠,一上来就能看懂。

三种提取模式

模式一:按帧率提取(默认)

设定每秒抽多少帧。举个例子:

  1. 原视频 30fps,设置 1:每秒抽 1 帧(每 30 帧取 1 帧)
  2. 原视频 30fps,设置 2:每秒抽 2 帧(每 15 帧取 1 帧)
  3. 原视频 60fps,设置 1:每秒抽 1 帧(每 60 帧取 1 帧)

适合那种希望均匀采样、不关心原始帧率的场景。

模式二:按时间间隔提取

指定每隔多少秒抽一帧:

  • 0.5:每 0.5 秒一帧
  • 2:每 2 秒一帧
  • 10:每 10 秒一帧

适合长视频,只想在关键时间点取几帧就够了。

模式三:全帧导出

视频里有多少帧就导出多少张。不需要填参数。

注意:一段 10 分钟 30fps 的视频有 18000 帧,硬盘空间不够的话会很惨。

输出文件命名

{变量名} 占位符拼出想要的文件名:

变量含义示例
{name}视频文件名(去扩展名)demo_video
{num}导出序号,从0开始0,1,2,..
{num:06d}补零到6为的序号000000,000001,…
{frame}视频原始帧号0,30,60,…
{ts}当前帧的时间戳(秒)0.00,1.00,…

默认规则:{name}_{num:06d}

假设视频是 demo.mp4,导出 JPG,输出结构如下:

输出目录/
└── demo/
├── demo_000000.jpg
├── demo_000001.jpg
├── demo_000002.jpg
└── …

想混搭也行,比如 {name}_frame{frame}_ts{ts}。每个视频会自动建一个同名子文件夹,不用担心不同视频的图片混一起。

JPG 还是 PNG?

特性JPGPNG
文件大小
画质有损无损
可调参数质量50-100,默认95压缩级别固定 3
推荐场景模型训练、日常使用需要逐像素无损保留

训练 YOLO 的话直接 JPG 质量 95,文件小、读得快,画质肉眼基本看不出差异。

多线程写入是怎么做的

视频解码和图片写入分两条路走:主线程一条一条读帧解码,线程池负责把解码好的帧并行写入磁盘。

线程数从 1 到 16 可调,默认 4。4 核 8 线程的 CPU 设 4-8 就够用,配置更高的可以再往上加。

这样做的好处是解码不用等写入,尤其在批量处理多段视频的时候,时间差还是挺明显的。

支持的格式

底层走 OpenCV 的 cv2.VideoCapture,理论上它能解码的格式都能用:

格式扩展名备注
MP4.mp4最常用,H.264/H.265
AVI.avi兼容性好得出奇
MKV.mkv高清视频常用封装
MOV.movApple 生态
WMV.wmvWindows 老格式
FLV.flvFlash 时代遗留
WebM.webm网页视频
M4V.m4viTunes 视频
TS.ts流媒体切片
MPG/MPEG.MPG,.MPEGMPEG 编码

如果遇到打不开的视频,大概率是缺对应的编解码器。程序会跳过并打一条警告日志,不会让整个批处理挂掉。

怎么安装?

第一步:装 Python

需要 Python 3.7+,推荐 3.10 以上。

Windows

Python 官网 ,点黄色 Download 按钮下载最新版。运行安装程序的时候,记得勾上 Add
Python to PATH,这步漏了后面会很折腾。装完之后打开 cmd,输 python –version,能看到版本号就说明 OK 了。

macOS:

brew install python@3.12

也可以去官网下安装包。

Linux (Ubuntu/Debian):

sudo apt update
sudo apt install python3 python3-pip

第二步:装依赖

pip install opencv-python numpy

下得慢就换国内源:

pip install opencv-python numpy -i https://pypi.tuna.tsinghua.edu.cn/simple

就两个依赖:opencv-python 负责视频解码和图片编码,numpy 是 OpenCV 底层用的。GUI 用了 tkinter,Python
自带的,不用额外装任何第三方 GUI 框架。

第三步:跑起来

把 video_to_frames.py 下到本地,终端里:

python video_to_frames.py

窗口弹出来就能用了。

使用流程

  1. 添加视频——点「添加视频」选文件,或「添加文件夹」批量导入
  2. 选提取模式——按帧率 / 按时长间隔 / 全帧,填好参数
  3. 选输出格式——JPG 或 PNG,JPG 可以拖滑块调质量
  4. 命名规则——用默认的或者自己写
  5. 选输出目录——点浏览找个位置
  6. 开始转换——点按钮,看进度条跑完

一些使用建议

  1. 选「添加文件夹」后程序会递归搜所有子目录里的视频,一次性处理大量视频很方便
  2. 处理中途可以随时点停止,已经保存的帧不会丢
  3. 每个视频的帧图片放在独立子文件夹里,文件名带了视频名前缀,不会弄混来源
  4. 线程数不是越大越好,一般不超过 CPU 逻辑核心数。不确定的话去任务管理器 → 性能里看一眼

技术栈

技术用途
Python 3语言
tkinterGUI(Python 内置)
OpenCV (cv2)视频解码、图片编码
numpy数组运算
concurrent.futures多线程并行写入
threading后台处理线程
pathlib路径和文件操作

最后说两句

代码不到 400 行,但该有的功能都有。不是什么大作,就是解决了一个很具体的问题——给模型训练准备视频帧数据。

如果你也在搞 YOLO 或者其他需要图片数据集的深度学习项目,也许用得上。

源码

由于不大就懒得上传Github了,直接复制写进文件内保存即可。
也可以下载我构建的成品:点击跳转下载

"""
视频转帧图片 GUI 工具
功能:批量视频提取帧、自定义帧率、自定义命名、多线程加速

依赖安装:pip install opencv-python numpy

------------------------------------------------------
@author    慕尘空
@date      2025-06-05
@website   https://www.xxkj.pro
------------------------------------------------------
"""

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import cv2
import numpy as np
import os
import threading
import time
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
from queue import Queue


class VideoToFramesApp:
    def __init__(self, root):
        self.root = root
        self.root.title("视频转帧图片工具")
        self.root.geometry("780x720")
        self.root.minsize(700, 620)

        self.video_files = []
        self.is_running = False

        self._build_ui()

    def _build_ui(self):
        # 文件选择区
        file_frame = ttk.LabelFrame(self.root, text="视频文件", padding=10)
        file_frame.pack(fill=tk.X, padx=10, pady=(10, 5))

        btn_row = ttk.Frame(file_frame)
        btn_row.pack(fill=tk.X)

        ttk.Button(btn_row, text="添加视频", command=self._add_files).pack(side=tk.LEFT, padx=(0, 5))
        ttk.Button(btn_row, text="添加文件夹", command=self._add_folder).pack(side=tk.LEFT, padx=(0, 5))
        ttk.Button(btn_row, text="移除选中", command=self._remove_selected).pack(side=tk.LEFT, padx=(0, 5))
        ttk.Button(btn_row, text="清空列表", command=self._clear_files).pack(side=tk.LEFT)

        list_frame = ttk.Frame(file_frame)
        list_frame.pack(fill=tk.BOTH, expand=True, pady=(8, 0))

        self.file_listbox = tk.Listbox(list_frame, height=6, selectmode=tk.EXTENDED)
        self.file_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.file_listbox.yview)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.file_listbox.config(yscrollcommand=scrollbar.set)

        # 参数设置
        settings_frame = ttk.LabelFrame(self.root, text="参数设置", padding=10)
        settings_frame.pack(fill=tk.X, padx=10, pady=5)

        # 提取模式
        row1 = ttk.Frame(settings_frame)
        row1.pack(fill=tk.X, pady=(0, 6))

        ttk.Label(row1, text="提取模式:").pack(side=tk.LEFT)
        self.mode_var = tk.StringVar(value="fps")
        ttk.Radiobutton(row1, text="按帧率", variable=self.mode_var, value="fps",
                        command=self._on_mode_change).pack(side=tk.LEFT, padx=(0, 10))
        ttk.Radiobutton(row1, text="按间隔(秒)", variable=self.mode_var, value="interval",
                        command=self._on_mode_change).pack(side=tk.LEFT, padx=(0, 10))
        ttk.Radiobutton(row1, text="全部帧", variable=self.mode_var, value="all",
                        command=self._on_mode_change).pack(side=tk.LEFT)

        ttk.Label(row1, text="  值:").pack(side=tk.LEFT)
        self.fps_var = tk.StringVar(value="1")
        self.fps_entry = ttk.Entry(row1, textvariable=self.fps_var, width=6)
        self.fps_entry.pack(side=tk.LEFT, padx=(0, 8))
        self.fps_hint = ttk.Label(row1, text="每秒提取1帧", foreground="gray")
        self.fps_hint.pack(side=tk.LEFT)

        # 输出格式 + 质量 + 线程数
        row2 = ttk.Frame(settings_frame)
        row2.pack(fill=tk.X, pady=(0, 6))

        ttk.Label(row2, text="输出格式:").pack(side=tk.LEFT)
        self.format_var = tk.StringVar(value="jpg")
        ttk.Radiobutton(row2, text="JPG", variable=self.format_var, value="jpg").pack(side=tk.LEFT, padx=(0, 8))
        ttk.Radiobutton(row2, text="PNG", variable=self.format_var, value="png").pack(side=tk.LEFT, padx=(0, 15))

        ttk.Label(row2, text="JPG质量:").pack(side=tk.LEFT)
        self.quality_var = tk.IntVar(value=95)
        ttk.Scale(row2, from_=50, to=100, variable=self.quality_var,
                  orient=tk.HORIZONTAL, length=100).pack(side=tk.LEFT, padx=(0, 3))
        self.quality_label = ttk.Label(row2, text="95", width=3)
        self.quality_label.pack(side=tk.LEFT, padx=(0, 15))
        self.quality_var.trace_add("write", lambda *_: self.quality_label.config(text=str(self.quality_var.get())))

        ttk.Label(row2, text="写入线程:").pack(side=tk.LEFT)
        self.threads_var = tk.StringVar(value="4")
        threads_combo = ttk.Combobox(row2, textvariable=self.threads_var, width=4, state="readonly",
                                     values=["1", "2", "4", "8", "12", "16"])
        threads_combo.pack(side=tk.LEFT)

        # 命名规则
        row3 = ttk.Frame(settings_frame)
        row3.pack(fill=tk.X, pady=(0, 6))

        ttk.Label(row3, text="命名规则:").pack(side=tk.LEFT)
        self.naming_var = tk.StringVar(value="{name}_{num:06d}")
        naming_entry = ttk.Entry(row3, textvariable=self.naming_var, width=35)
        naming_entry.pack(side=tk.LEFT, padx=(0, 8))

        ttk.Label(row3, text="变量:", foreground="gray").pack(side=tk.LEFT)
        help_text = "{name} {num} {frame} {ts}"
        ttk.Label(row3, text=help_text, foreground="blue").pack(side=tk.LEFT)

        row3b = ttk.Frame(settings_frame)
        row3b.pack(fill=tk.X, pady=(0, 6))
        ttk.Label(row3b, text="    说明:{name}=视频名  {num}=序号  {num:06d}=补零序号  "
                             "{frame}=原始帧号  {ts}=时间戳(秒)", foreground="gray").pack(side=tk.LEFT)

        # 输出目录
        row4 = ttk.Frame(settings_frame)
        row4.pack(fill=tk.X)

        ttk.Label(row4, text="输出目录:").pack(side=tk.LEFT)
        self.output_var = tk.StringVar()
        ttk.Entry(row4, textvariable=self.output_var, width=45).pack(side=tk.LEFT, padx=(0, 5), fill=tk.X, expand=True)
        ttk.Button(row4, text="浏览", command=self._select_output).pack(side=tk.LEFT)

        # 进度和控制
        progress_frame = ttk.LabelFrame(self.root, text="转换进度", padding=10)
        progress_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(5, 10))

        self.progress_var = tk.DoubleVar(value=0)
        self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, maximum=100)
        self.progress_bar.pack(fill=tk.X, pady=(0, 5))

        status_row = ttk.Frame(progress_frame)
        status_row.pack(fill=tk.X)
        self.status_var = tk.StringVar(value="就绪")
        ttk.Label(status_row, textvariable=self.status_var).pack(side=tk.LEFT)
        self.speed_var = tk.StringVar(value="")
        ttk.Label(status_row, textvariable=self.speed_var, foreground="green").pack(side=tk.RIGHT)

        self.log_text = tk.Text(progress_frame, height=6, state=tk.DISABLED, font=("Consolas", 9))
        self.log_text.pack(fill=tk.BOTH, expand=True, pady=(5, 0))

        log_scroll = ttk.Scrollbar(self.log_text, orient=tk.VERTICAL, command=self.log_text.yview)
        log_scroll.pack(side=tk.RIGHT, fill=tk.Y)
        self.log_text.config(yscrollcommand=log_scroll.set)

        btn_frame = ttk.Frame(self.root)
        btn_frame.pack(fill=tk.X, padx=10, pady=(0, 10))

        self.about_btn = ttk.Button(btn_frame, text="关于", command=self._show_about)
        self.about_btn.pack(side=tk.LEFT)

        self.start_btn = ttk.Button(btn_frame, text="开始转换", command=self._start)
        self.start_btn.pack(side=tk.RIGHT, padx=(5, 0))
        self.stop_btn = ttk.Button(btn_frame, text="停止", command=self._stop, state=tk.DISABLED)
        self.stop_btn.pack(side=tk.RIGHT)

    def _on_mode_change(self):
        mode = self.mode_var.get()
        if mode == "fps":
            self.fps_entry.config(state=tk.NORMAL)
            self.fps_hint.config(text="每秒提取N帧")
        elif mode == "interval":
            self.fps_entry.config(state=tk.NORMAL)
            self.fps_hint.config(text="每隔N秒提取1帧")
        else:
            self.fps_entry.config(state=tk.DISABLED)
            self.fps_hint.config(text="提取全部帧(文件可能非常多)")

    def _show_about(self):
        messagebox.showinfo(
            "关于",
            "视频转帧图片工具\n\n"
            "版本:v1.0\n"
            "作者:慕尘空\n"
            "日期:2025-06-05\n"
            "网站:https://www.xxkj.pro"
        )

    def _add_files(self):
        files = filedialog.askopenfilenames(
            title="选择视频文件",
            filetypes=[
                ("视频文件", "*.mp4 *.avi *.mkv *.mov *.wmv *.flv *.webm *.m4v *.ts *.mpg *.mpeg"),
                ("所有文件", "*.*")
            ]
        )
        for f in files:
            if f not in self.video_files:
                self.video_files.append(f)
                self.file_listbox.insert(tk.END, os.path.basename(f))

    def _add_folder(self):
        folder = filedialog.askdirectory(title="选择包含视频的文件夹")
        if not folder:
            return
        video_exts = {'.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.ts', '.mpg', '.mpeg'}
        for f in Path(folder).rglob("*"):
            if f.suffix.lower() in video_exts and str(f) not in self.video_files:
                self.video_files.append(str(f))
                self.file_listbox.insert(tk.END, f.name)

    def _remove_selected(self):
        selected = list(self.file_listbox.curselection())
        for i in reversed(selected):
            self.file_listbox.delete(i)
            del self.video_files[i]

    def _clear_files(self):
        self.video_files.clear()
        self.file_listbox.delete(0, tk.END)

    def _select_output(self):
        folder = filedialog.askdirectory(title="选择输出目录")
        if folder:
            self.output_var.set(folder)

    def _log(self, msg):
        self.log_text.config(state=tk.NORMAL)
        self.log_text.insert(tk.END, msg + "\n")
        self.log_text.see(tk.END)
        self.log_text.config(state=tk.DISABLED)

    def _format_filename(self, template, video_name, num, frame_idx, timestamp, fmt):
        try:
            filename = template.format(
                name=video_name,
                num=num,
                frame=frame_idx,
                ts=f"{timestamp:.2f}"
            )
        except (KeyError, ValueError, IndexError):
            filename = f"{video_name}_{num:06d}"
        return filename + f".{fmt}"

    def _start(self):
        if not self.video_files:
            messagebox.showwarning("提示", "请先添加视频文件")
            return

        output_dir = self.output_var.get().strip()
        if not output_dir:
            messagebox.showwarning("提示", "请选择输出目录")
            return

        mode = self.mode_var.get()
        if mode != "all":
            try:
                value = float(self.fps_var.get())
                if value <= 0:
                    raise ValueError
            except ValueError:
                messagebox.showwarning("提示", "请输入有效的正数值")
                return

        naming = self.naming_var.get().strip()
        if not naming:
            messagebox.showwarning("提示", "命名规则不能为空")
            return

        self.is_running = True
        self.start_btn.config(state=tk.DISABLED)
        self.stop_btn.config(state=tk.NORMAL)
        self.progress_var.set(0)
        self.speed_var.set("")

        self.log_text.config(state=tk.NORMAL)
        self.log_text.delete(1.0, tk.END)
        self.log_text.config(state=tk.DISABLED)

        thread = threading.Thread(target=self._process, daemon=True)
        thread.start()

    def _stop(self):
        self.is_running = False
        self.status_var.set("正在停止...")

    def _write_frame(self, filepath, frame, fmt, quality):
        """在线程池中执行的帧写入任务"""
        if fmt == "jpg":
            _, buf = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, quality])
        else:
            _, buf = cv2.imencode(".png", frame, [cv2.IMWRITE_PNG_COMPRESSION, 3])
        buf.tofile(filepath)

    def _process(self):
        output_dir = self.output_var.get().strip()
        mode = self.mode_var.get()
        fmt = self.format_var.get()
        quality = self.quality_var.get()
        value = float(self.fps_var.get()) if mode != "all" else 0
        naming = self.naming_var.get().strip()
        num_threads = int(self.threads_var.get())

        total_files = len(self.video_files)
        total_frames_saved = 0
        overall_start = time.time()

        for idx, video_path in enumerate(self.video_files):
            if not self.is_running:
                break

            video_name = Path(video_path).stem
            video_output = os.path.join(output_dir, video_name)
            os.makedirs(video_output, exist_ok=True)

            self.root.after(0, lambda v=video_name: self.status_var.set(f"正在处理: {v}"))
            self.root.after(0, lambda m=f"[{idx+1}/{total_files}] 开始处理: {video_name}": self._log(m))

            cap = cv2.VideoCapture(video_path)
            if not cap.isOpened():
                cap = cv2.VideoCapture()
                cap.open(video_path, cv2.CAP_ANY)
            if not cap.isOpened():
                self.root.after(0, lambda: self._log("  ⚠ 无法打开视频,跳过"))
                continue

            video_fps = cap.get(cv2.CAP_PROP_FPS)
            total_frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))

            if video_fps <= 0:
                video_fps = 30.0

            if mode == "fps":
                frame_interval = max(1, int(video_fps / value))
            elif mode == "interval":
                frame_interval = max(1, int(video_fps * value))
            else:
                frame_interval = 1

            frame_idx = 0
            saved_count = 0
            file_start = time.time()

            executor = ThreadPoolExecutor(max_workers=num_threads)
            futures = []

            while self.is_running:
                ret, frame = cap.read()
                if not ret:
                    break

                if frame_idx % frame_interval == 0:
                    timestamp = frame_idx / video_fps
                    filename = self._format_filename(naming, video_name, saved_count, frame_idx, timestamp, fmt)
                    filepath = os.path.join(video_output, filename)

                    frame_copy = frame.copy()
                    future = executor.submit(self._write_frame, filepath, frame_copy, fmt, quality)
                    futures.append(future)

                    saved_count += 1

                    if len(futures) >= num_threads * 10:
                        for f in futures:
                            f.result()
                        futures.clear()

                frame_idx += 1

                if frame_idx % 200 == 0 and total_frame_count > 0:
                    file_progress = frame_idx / total_frame_count
                    overall = (idx + file_progress) / total_files * 100
                    elapsed = time.time() - file_start
                    fps_speed = frame_idx / elapsed if elapsed > 0 else 0
                    self.root.after(0, lambda p=overall: self.progress_var.set(p))
                    self.root.after(0, lambda s=fps_speed: self.speed_var.set(f"处理速度: {s:.0f} 帧/秒"))

            for f in futures:
                f.result()
            futures.clear()
            executor.shutdown(wait=True)

            cap.release()
            total_frames_saved += saved_count
            elapsed = time.time() - file_start
            self.root.after(0, lambda c=saved_count, t=elapsed: self._log(
                f"  ✓ 完成,保存 {c} 帧,耗时 {t:.1f}秒"))

            overall = (idx + 1) / total_files * 100
            self.root.after(0, lambda p=overall: self.progress_var.set(p))

        total_time = time.time() - overall_start

        if self.is_running:
            self.root.after(0, lambda: self.status_var.set(
                f"全部完成!共保存 {total_frames_saved} 帧"))
            self.root.after(0, lambda: self._log(
                f"\n全部完成,共保存 {total_frames_saved} 帧,总耗时 {total_time:.1f}秒"))
            self.root.after(0, lambda: self.speed_var.set(
                f"平均 {total_frames_saved/total_time:.0f} 帧/秒" if total_time > 0 else ""))
        else:
            self.root.after(0, lambda: self.status_var.set("已停止"))
            self.root.after(0, lambda: self._log(
                f"\n已停止,当前已保存 {total_frames_saved} 帧"))

        self.root.after(0, lambda: self.start_btn.config(state=tk.NORMAL))
        self.root.after(0, lambda: self.stop_btn.config(state=tk.DISABLED))
        self.is_running = False


if __name__ == "__main__":
    root = tk.Tk()
    app = VideoToFramesApp(root)
    root.mainloop()
觉得有帮助可以投喂下博主哦~感谢!
作者:慕尘空
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0协议
转载请注明文章地址及作者哦~
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇