草庐IT

YOLOX模型部署Android端-NCNN方法

勇敢牛牛@ 2023-04-23 原文

YOLOX模型部署Android端-NCNN方法


将自己任务的YOLOX-nano模型和YOLOX-tiny模型通过NCNN架构的转换方式部署到Android手机端。

1.YOLOX代码仓库的下载

对于自己的任务,需要修改以下几处代码:

  • 1.1数据集的准备(使用的VOC数据格式):在datasets文件夹下新建一个VOCdevkit文件夹->接着在VOCdevkit文件夹下新建VOC2007文件夹->接着在VOC2007文件夹下新建Annotations、ImageSets、JPEGImages三个文件夹,分别存储.xml标签文件、划分数据集为训练集和验证集的文本、和图片数据。
    大概如下图所示:
  • 1.2 根据自己的任务修改几处代码:(1)如果我们使用YOLOX-nano模型,建议在.\exps\example\yolox_voc\文件夹下新建一个yolox_voc_nano.py,该文件与yolox_voc_s.py并列。yolox_voc_nano.py的代码是参考别人的,如下所示:
# encoding: utf-8
import os
import torch
# 需要加上这个
import torch.nn as nn
import torch.distributed as dist
from yolox.data import get_yolox_datadir
from yolox.exp import Exp as MyExp
class Exp(MyExp):
    def __init__(self):
        super(Exp, self).__init__()
        
        # 修改网络深度和宽度
        self.depth = 0.33
        self.width = 0.25
        
        self.input_size = (416, 416)
        self.mosaic_scale = (0.5, 1.5)
        self.random_size = (10, 20)
        self.test_size = (416, 416)
        self.exp_name = os.path.split(os.path.realpath(__file__))[1].split(".")[0]
        self.enable_mixup = False
        
        # 修改类别数
        self.num_classes = 1
  
# 之前没有加上这个get_model函数,就训练有问题
def get_model(self, sublinear=False):

    def init_yolo(M):
        for m in M.modules():
            if isinstance(m, nn.BatchNorm2d):
                m.eps = 1e-3
                m.momentum = 0.03
    if "model" not in self.__dict__:
        from yolox.models import YOLOX, YOLOPAFPN, YOLOXHead
        in_channels = [256, 512, 1024]
        # NANO model use depthwise = True, which is main difference.
        backbone = YOLOPAFPN(self.depth, self.width, in_channels=in_channels, depthwise=True)
        head = YOLOXHead(self.num_classes, self.width, in_channels=in_channels, depthwise=True)
        self.model = YOLOX(backbone, head)

    self.model.apply(init_yolo)
    self.model.head.initialize_biases(1e-2)
    return self.model

def get_data_loader(self, batch_size, is_distributed, no_aug=False, cache_img=False):
    from yolox.data import (
        VOCDetection,
        TrainTransform,
        YoloBatchSampler,
        DataLoader,
        InfiniteSampler,
        MosaicDetection,
        worker_init_reset_seed,
    )
    from yolox.utils import (
        wait_for_the_master,
        get_local_rank,
    )
    local_rank = get_local_rank()

    with wait_for_the_master(local_rank):
        dataset = VOCDetection(
            data_dir=os.path.join(get_yolox_datadir(), "VOCdevkit"),
            # image_sets=[('2007', 'trainval'), ('2012', 'trainval')],
            # 训练的时候只有VOC2007的数据集,所以需要改这里
            image_sets=[('2007', 'trainval')],
            img_size=self.input_size,
            preproc=TrainTransform(
                max_labels=50,
                flip_prob=self.flip_prob,
                hsv_prob=self.hsv_prob),
            cache=cache_img,
        )

    dataset = MosaicDetection(
        dataset,
        mosaic=not no_aug,
        img_size=self.input_size,
        preproc=TrainTransform(
            max_labels=120,
            flip_prob=self.flip_prob,
            hsv_prob=self.hsv_prob),
        degrees=self.degrees,
        translate=self.translate,
        mosaic_scale=self.mosaic_scale,
        mixup_scale=self.mixup_scale,
        shear=self.shear,
        enable_mixup=self.enable_mixup,
        mosaic_prob=self.mosaic_prob,
        mixup_prob=self.mixup_prob,
    )

    self.dataset = dataset

    if is_distributed:
        batch_size = batch_size // dist.get_world_size()

    sampler = InfiniteSampler(
        len(self.dataset), seed=self.seed if self.seed else 0
    )

    batch_sampler = YoloBatchSampler(
        sampler=sampler,
        batch_size=batch_size,
        drop_last=False,
        mosaic=not no_aug,
    )

    dataloader_kwargs = {"num_workers": self.data_num_workers, "pin_memory": True}
    dataloader_kwargs["batch_sampler"] = batch_sampler

    # Make sure each process has different random seed, especially for 'fork' method
    dataloader_kwargs["worker_init_fn"] = worker_init_reset_seed

    train_loader = DataLoader(self.dataset, **dataloader_kwargs)

    return train_loader

def get_eval_loader(self, batch_size, is_distributed, testdev=False, legacy=False):
    from yolox.data import VOCDetection, ValTransform

    valdataset = VOCDetection(
        data_dir=os.path.join(get_yolox_datadir(), "VOCdevkit"),
        image_sets=[('2007', 'test')],
        img_size=self.test_size,
        preproc=ValTransform(legacy=legacy),
    )

    if is_distributed:
        batch_size = batch_size // dist.get_world_size()
        sampler = torch.utils.data.distributed.DistributedSampler(
            valdataset, shuffle=False
        )
    else:
        sampler = torch.utils.data.SequentialSampler(valdataset)

    dataloader_kwargs = {
        "num_workers": self.data_num_workers,
        "pin_memory": True,
        "sampler": sampler,
    }
    dataloader_kwargs["batch_size"] = batch_size
    val_loader = torch.utils.data.DataLoader(valdataset, **dataloader_kwargs)

    return val_loader

def get_evaluator(self, batch_size, is_distributed, testdev=False, legacy=False):
    from yolox.evaluators import VOCEvaluator

    val_loader = self.get_eval_loader(batch_size, is_distributed, testdev, legacy)
    evaluator = VOCEvaluator(
        dataloader=val_loader,
        img_size=self.test_size,
        confthre=self.test_conf,
        nmsthre=self.nmsthre,
        num_classes=self.num_classes,
    )
    return evaluator

当然yolox_voc_nano.py代码你也可以不用新建(只训练nano模型时),只需要在yolox_voc_s.py代码的基础上进行修改(nano和tiny模型都在这上面修改即可,类别数及模型大小)。self.num_classes 类别数、self.depth = 0.33、self.width = 0.25(nano模型:0.33,0.25;tiny模型:0.33,0.375)以及wait_for_the_master函数下的data_dir=路径直接修改为数据集的绝对路径,比如:data_dir=“E:\Android_studio\YOLOX\datasets\VOCdevkit”,而下面的image_sets函数修改为:image_sets=[(‘2007’, ‘trainval’)],。同理,下面的get_eval_loade函数这两处也修改绝对路径和image_sets=[(‘2007’, ‘test’)],。
(2)还需要修改.\yolox\exp\yolox_base.py文件。其中的self.num_classes 类别数、self.depth = 0.33、self.width = 0.25(nano模型:0.33,0.25;tiny模型:0.33,0.375),self.input_size =(416,416)建议为416。其中还可以根据自己的调参习惯进行其余项的修改,最大的epoch数,热重启的学习率,以及多少个epoch进行验证等。
(3)接下来就是.\tools\train.py文件的修改。其中-b为batch size的大小,-f需要修改:default=“exps/example/yolox_voc/yolox_voc_s.py”,-c是加载预训练模型default="weights/yolox_nano.pth"其余的按着自己电脑配置自行设置。
(4)最后就是模型的训练了。

2.ONNX模型转换

默认我们已经训练好了自己的模型,并得到了yolox_nano.pth和yolox_tiny.pth(就是每次得到的best_ckpt.pth进行重命名就可以)。

  • 使用YOLOX官方自带的export_onnx.py进行转换。其中**–output-name需要修改转换模型的保存路径,-f同样需要进行指定文件default="exps/example/yolox_voc/yolox_voc_s.py,-c加载的就是我们训练好的.pt的权值文件。转换提示如下图:(nano和tiny模型需要修改两次,-f指定的yolox_voc_s.py文件需要按着每次转换的模型修改深度和宽度的系数)

3.NCNN框架模型转换

接下来就稍微有些麻烦了~~

  • 3.1 安装protobuf:下载后解压:https://github.com/google/protobuf/archive/v3.4.0.zip ,然后使用

    绿色终端执行命令:
cd <protobuf-root-dir>
mkdir build-vs2017
cd build-vs2017
cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -
DCMAKE_INSTALL_PREFIX=%cd%/install -Dprotobuf_BUILD_TESTS=OFF -
Dprotobuf_MSVC_STATIC_RUNTIME=OFF ../cmake
nmake
nmake install

编译后执行,验证是否安装。

protoc.exe --version
  • 3.2 编译NCNN,从该地址下载:https://github.com/Tencent/ncnn/releases。同样打开刚才的(VS2017X64)的命令终端(进入到ncnn根目录下),执行以下语句:
cd <ncnn-root-dir>
mkdir -p build-vs2017
cd build-vs2017
cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -
DCMAKE_INSTALL_PREFIX=%cd%/install -DProtobuf_INCLUDE_DIR=D:/protobuf3.4.0/build-vs2019/install/include -DProtobuf_LIBRARIES=D:/protobuf-3.4.0/buildvs2019/install/lib/libprotobuf.lib -DProtobuf_PROTOC_EXECUTABLE=D:/protobuf3.4.0/build-vs2019/install/bin/protoc.exe -DNCNN_VULKAN=OFF ..
nmake
nmake install

编译后.\ncnn\build-vs2019\tools\onnx下有onnx2ncnn.exe
注:这里其实还可以使用另一种官方的库,实在是忘记参考哪个博客下载的,该文件下包含转换的exe程序,而且不需要下载编译,可直接用该文件夹.\X64\bin\下的onnx2ncnn.exe,ncnnoptimize.exe:网盘地址:链接:https://pan.baidu.com/s/1aKNmAvApLsnKtBl0BXA22Q
提取码:8ewm

  • 3.3 生成NCNN文件。拷贝yolox_nano.onnx和yolox_tiny.onnx文件到onnx2ncnn.exe,ncnnoptimize.exe文件下,生成ncnn相应的param和bin文件。(命令如下)
onnx2ncnn.exe yolox_nano.onnx yolox_nano.param yolox_nano.bin
onnx2ncnn.exe yolox_tiny.onnx yolox_tiny.param yolox_tiny.bin

因为ncnn不支持Focus模块,会有警告:(没关系,不用管)

Unsupported slice step ! 
Unsupported slice step ! 
Unsupported slice step ! 
  • 3.4 因为ncnn不支持Focus模块,所以需要对yolox_nano.param、yolox_tiny.param进行修改。
    (1)yolo_nano.param模型修改前:
7767517
295 328
Input           images                   0 1 images
Split           splitncnn_input0         1 4 images images_splitncnn_0
images_splitncnn_1 images_splitncnn_2 images_splitncnn_3
Crop             Slice_4                 1 1 images_splitncnn_3 647 -23309=1,0
-23310=1,2147483647 -23311=1,1
Crop             Slice_9                 1 1 647 652 -23309=1,0
-23310=1,2147483647 -23311=1,2
Crop             Slice_14                 1 1 images_splitncnn_2 657 -23309=1,0
-23310=1,2147483647 -23311=1,1
Crop             Slice_19                 1 1 657 662 -23309=1,1
-23310=1,2147483647 -23311=1,2
Crop             Slice_24                 1 1 images_splitncnn_1 667 -23309=1,1
-23310=1,2147483647 -23311=1,1
Crop             Slice_29                 1 1 667 672 -23309=1,0
-23310=1,2147483647 -23311=1,2
Crop             Slice_34                 1 1 images_splitncnn_0 677 -23309=1,1
-23310=1,2147483647 -23311=1,1
Crop             Slice_39                 1 1 677 682 -23309=1,1
-23310=1,2147483647 -23311=1,2
Concat           Concat_40               4 1 652 672 662 682 683 0=0
Convolution     Conv_41                 1 1 683 1177 0=16 1=3 11=3 2=1 12=1
3=1 13=1 4=1 14=1 15=1 16=1 5=1 6=1728
Swish           Mul_43                   1 1 1177 687
ConvolutionDepthWise Conv_44                 1 1 687 1180 0=16 1=3 11=3 2=1
12=1 3=2 13=2 4=1 14=1 15=1 16=1 5=1 6=144 7=16
Swish           Mul_46                   1 1 1180 691
Convolution     Conv_47                 1 1 691 1183 0=32 1=1 11=1 2=1 12=1
3=1 13=1 4=0 14=0 15=0 16=0 5=1 6=512
Swish           Mul_49                   1 1 1183 695

把 295修改为295 - 9 = 286 (由于我们将删除 10 层并添加 1 层,因此总层数应减去 9)。然后从 Split 到 Concat 删除 10 行代码,但记住Concat一行最后倒数第二个数字:683。在输入后添加 YoloV5Focus 层(使用之前的数字 683):

YoloV5Focus     focus                   1 1 images 683

yolox_nano.param文件修改后:(注:YoloV5Focus一定不要写错大小写,app闪退就是因为大小写)

286 328
Input           images                   0 1 images
YoloV5Focus     focus                   1 1 images 683

(2)yolo_tiny.param模型修改前:

7767517
235 268
Input           images                   0 1 images
Split           splitncnn_input0         1 4 images images_splitncnn_0
images_splitncnn_1 images_splitncnn_2 images_splitncnn_3
Crop             Slice_4                 1 1 images_splitncnn_3 467 -23309=1,0
-23310=1,2147483647 -23311=1,1
Crop             Slice_9                 1 1 467 472 -23309=1,0
-23310=1,2147483647 -23311=1,2
Crop             Slice_14                 1 1 images_splitncnn_2 477 -23309=1,0
-23310=1,2147483647 -23311=1,1
Crop             Slice_19                 1 1 477 482 -23309=1,1
-23310=1,2147483647 -23311=1,2
Crop             Slice_24                 1 1 images_splitncnn_1 487 -23309=1,1
-23310=1,2147483647 -23311=1,1
Crop             Slice_29                 1 1 487 492 -23309=1,0
-23310=1,2147483647 -23311=1,2
Crop             Slice_34                 1 1 images_splitncnn_0 497 -23309=1,1
-23310=1,2147483647 -23311=1,1
Crop             Slice_39                 1 1 497 502 -23309=1,1
-23310=1,2147483647 -23311=1,2
Concat           Concat_40               4 1 472 492 482 502 503 0=0
Convolution     Conv_41                 1 1 503 877 0=24 1=3 11=3 2=1 12=1 3=1
13=1 4=1 14=1 15=1 16=1 5=1 6=2592
Swish           Mul_43                   1 1 877 507
Convolution     Conv_44                 1 1 507 880 0=48 1=3 11=3 2=1 12=1 3=2
13=2 4=1 14=1 15=1 16=1 5=1 6=10368
Swish           Mul_46                   1 1 880 511
Split           splitncnn_0             1 2 511 511_splitncnn_0
511_splitncnn_1
Convolution     Conv_47                 1 1 511_splitncnn_1 883 0=24 1=1 11=1
2=1 12=1 3=1 13=1 4=0 14=0 15=0 16=0 5=1 6=1152

把 235修改为235 - 9 = 226 (由于我们将删除 10 层并添加 1 层,因此总层数应减去 9);然后从 Split 到 Concat 删除 10 行代码,记住Concat一行最后倒数第二个数字:503。在输入后添加 YoloV5Focus 层(使用之前的数字 503)。修改后为:

7767517
226 268
Input           images                   0 1 images
YoloV5Focus     focus                   1 1 images 503

(3)对算子量化。使用ncnnoptimize.exe进行模型的量化。基于刚刚修改的.param和.bin文件,在终端输入:

ncnnoptimize.exe yolox_nano.param yolox_nano.bin yolox_nano.param yolox_nano.bin 65536

yolox_nano.param文件中开头的286改280,328改310(自动改的)

ncnnoptimize.exe yolox_tiny.param yolox_tiny.bin yolox_tiny.param yolox_tiny.bin 65536

yolox_tiny.param文件中开头的226改220,268改250(自动改的)
其中.param为模型的结构文件,.bin为模型的参数文件。

4 安装Android Studio

网址:https://developer.android.google.cn/studio/
安装时会提示安装SDK
注意:Android SDK安装路径中不要有空格
注意配置:
File->Settings->Appearance & Behavior ->System Settings->Android SDK
SDK Platforms选中面向手机的Android版本
SDK Tools选中NDK, CMake
(值得注意的是:校园网会出现加载不出来SDK Tools的选项,所以要使用手机热点)

5 准备Android项目文件

  • 5.1 首先下载Android开发文件:https://github.com/nihui/ncnn-android-nanodet(里面已经有参考和步骤,我下载好的:链接:https://pan.baidu.com/s/14S05OMCx-O2BAvMx5cnJdA 提取码:5epp)
  • 5.2 放置ncnn的安卓文件:https://github.com/Tencent/ncnn/releases
    下载ncnn-YYYYMMDD-android-vulkan.zip
    课程中使用ncnn-20210720-android-vulkan.zip
    解压ncnn-YYYYMMDD-android-vulkan.zip后放置到app/src/main/jni 并修改 app/src/main/jni/CMakeLists.txt中的ncnn_DIR
  • 5.3 放置opencv的安卓文件:https://github.com/nihui/opencv-mobile
    下载opencv-mobile-XYZ-android.zip
    课程中使用opencv-mobile-4.5.3-android.zip
    解压opencv-mobile-XYZ-android.zip后放置到app/src/main/jni并修改 app/src/main/jni/CMakeLists.txt中的 OpenCV_DIR
  • 5.4 修改CMakeLists.txt文件
project(ncnnyolox)
cmake_minimum_required(VERSION 3.10)
set(OpenCV_DIR ${CMAKE_SOURCE_DIR}/opencv-mobile-4.5.3-android/sdk/native/jni)
find_package(OpenCV REQUIRED core imgproc)
set(ncnn_DIR ${CMAKE_SOURCE_DIR}/ncnn-20210720-androidvulkan/${ANDROID_ABI}/lib/cmake/ncnn)
find_package(ncnn REQUIRED)
add_library(ncnnyolox SHARED yoloxncnn.cpp yolox.cpp ndkcamera.cpp)
target_link_libraries(ncnnyolox ncnn ${OpenCV_LIBS} camera2ndk mediandk)
  • 5.5 自己数据集训练模型的部署
    asset文件下,替换自己训练及转化的模型
    修改yolox.cpp和yoloxncnn.cpp文件中的class_names(自己按着自己的任务修改)

6 连接手机

  • 6.1 手机打开开发者模型,并打开开发者模式下的USB调试功能。
  • 6.2 此时手机界面弹出提示窗口,选择允许,Android Studio就会连接到手机了。
  • 6.3 点击右上方app左侧的绿色小锤子(不知道叫啥)再加载一次需要的环境,然后点击右侧的绿色三角开始键,apk就部署到手机上了。
    下面是我最新的部署结果:
    部署前:

    自己的模型部署后:

    有疑问的小伙伴可以联系我一起解决~~

有关YOLOX模型部署Android端-NCNN方法的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  4. ruby - Facter::Util::Uptime:Module 的未定义方法 get_uptime (NoMethodError) - 2

    我正在尝试设置一个puppet节点,但ruby​​gems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由ruby​​gems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby

  5. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  6. Ruby 方法() 方法 - 2

    我想了解Ruby方法methods()是如何工作的。我尝试使用“ruby方法”在Google上搜索,但这不是我需要的。我也看过ruby​​-doc.org,但我没有找到这种方法。你能详细解释一下它是如何工作的或者给我一个链接吗?更新我用methods()方法做了实验,得到了这样的结果:'labrat'代码classFirstdeffirst_instance_mymethodenddefself.first_class_mymethodendendclassSecond使用类#returnsavailablemethodslistforclassandancestorsputsSeco

  7. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  8. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  9. ruby-on-rails - 在混合/模块中覆盖模型的属性访问器 - 2

    我有一个包含模块的模型。我想在模块中覆盖模型的访问器方法。例如:classBlah这显然行不通。有什么想法可以实现吗? 最佳答案 您的代码看起来是正确的。我们正在毫无困难地使用这个确切的模式。如果我没记错的话,Rails使用#method_missing作为属性setter,因此您的模块将优先,阻止ActiveRecord的setter。如果您正在使用ActiveSupport::Concern(参见thisblogpost),那么您的实例方法需要进入一个特殊的模块:classBlah

  10. ruby - Highline 询问方法不会使用同一行 - 2

    设置:狂欢ruby1.9.2高线(1.6.13)描述:我已经相当习惯在其他一些项目中使用highline,但已经有几个月没有使用它了。现在,在Ruby1.9.2上全新安装时,它似乎不允许在同一行回答提示。所以以前我会看到类似的东西:require"highline/import"ask"Whatisyourfavoritecolor?"并得到:Whatisyourfavoritecolor?|现在我看到类似的东西:Whatisyourfavoritecolor?|竖线(|)符号是我的终端光标。知道为什么会发生这种变化吗? 最佳答案

随机推荐