LoginSignup
18
11

More than 1 year has passed since last update.

DeepStream SDK を C/C++ から利用する

Last updated at Posted at 2021-05-05

はじめに

DeepStream は NVIDIA の画像処理・動画分析のフレームワークです。AI を活用したアプリケーションの構築に利用できます。

物体検出などの基本的な処理を動かすだけならコードを書かずに実行できるのですが、ここではあえて C/C++ から利用してみようと思います。

準備

Jetson Nano のセットアップ

  • 今回は Jetson Nano 4GB版を使って実験しました。
  • JetPack 4.5.1 (L4T32.5.1) のイメージをダウンロードして 32GB の micro SD カードに焼き、セットアップしています。

DeepStream のインストール

  • DeepStream Documentation の Quickstart Guide に従ってセットアップします。
  • DeepStream のインストール方法は次の5通りあります。
    • ①SDK Managerを使う方法
    • ②tar packageからインストールする方法
    • ③Debian packageからインストールする方法
    • ④apt でインストールする方法
    • ⑤Docker コンテナを利用する方法
      今回は ④ を選択し、apt で deepstream-5.1 をインストールしました。
  • なお、JetPack 4.4~4.5 の場合は DeepStream 5.0 に対応しているので、NVIDIA DeepStream SDK on Jetson (Archived) からダウンロードしてインストールすることになると思います。
  • サンプルを実行してインストールできたか確認します。

    $ cd /opt/nvidia/deepstream/deepstream-5.1/samples/configs/deepstream-app
    $ deepstream-app -c source30_1080p_dec_infer-resnet_tiled_display_int8.txt
    

    推論が開始されるまで数分かかります。次のような画像が表示されたら成功です。(Jetson Nano には荷が重いようで少しカクついていました。)

GStreamer について

C/C++ サンプル

  • GStreamer の仕組みがなんとなく分かったところで、DeepStream のサンプルを見ていきます。サンプルソースコードは /opt/nvidia/deepstream/deepstream-5.1/sources/apps/sample_apps にあります。
  • それぞれのサンプルの説明は C/C++ Sample Apps Source Details にあります。これによると、deepstream-test1は H.264 コーデックの動画ファイルを入力として、物体検出を実行し、検出結果を表示するようです。
  • これ以降、deepstream-test1 を見ていきます。

全体の流れ

  • deepstream-test1 の全体の流れとしては、次のようになっています。
    1. filesrc: ファイルからデータを読み込みます。このサンプルでは H.264 コーデックの動画ファイルを想定しています。
    2. h264parse: H.264 をパースします。
    3. nvv4l2decoder: H.264 をデコードして NV12 フォーマットにし、NVMM バッファーに変換しているようです。
    4. nvstreammux: ひとつあるいは複数の入力をまとめてバッチにします。また、推論に必要なメタデータを追加します。
    5. nvinfer: 推論を実行します。推論結果はメタデータに格納されます。
    6. nvvideoconvert: 後段の nvdsosd が RGBA にしか対応していないため、NV12 を RGBA に変換しているようです。
    7. nvdsosd: OSD は On Screen Display の略で、推論結果を文字や矩形で描画します。
    8. nvegltransform: Jetson では後段の nveglglessink が NVMM に対応していないため、EGLImage に変換しているそうです。(参考)
    9. nveglglessink: ウィンドウに表示します。

ソースコード

  • サンプルを見ていきます。一番上はライセンス表記です。
/*
 * Copyright (c) 2018-2020, NVIDIA CORPORATION. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */
  • 以下で必要なヘッダーファイルをインクルードしています。gstnvdsmeta.h は DeepStream で使用されるメタデータの構造を定義するファイルで、/opt/nvidia/deepstream/deepstream-5.1/sources/includes にあります。
#include <gst/gst.h>
#include <glib.h>
#include <stdio.h>
#include <cuda_runtime_api.h>
#include "gstnvdsmeta.h"
  • 次は定数の定義です。
    • MAX_DISPLAY_LEN はOSDで表示する文字の最大数です。
    • PGIE は Primary GPU Inference Engine の略のようです。PGIE_CLASS_ID_VEHICLEPGIE_CLASS_ID_PERSON は検出した物体が「乗り物」か「人」かの判断に利用します。
    • MUXER_OUTPUT_WIDTHMUXER_OUTPUT_HEIGHT でバッチの画像サイズを指定しています。複数入力がある場合は全てこのサイズに揃えられるようです。
    • MUXER_BATCH_TIMEOUT_USEC は、複数の入力が揃うまで待つときのタイムアウト時間です。
#define MAX_DISPLAY_LEN 64

#define PGIE_CLASS_ID_VEHICLE 0
#define PGIE_CLASS_ID_PERSON 2

/* The muxer output resolution must be set if the input streams will be of
 * different resolution. The muxer will scale all the input frames to this
 * resolution. */
#define MUXER_OUTPUT_WIDTH 1920
#define MUXER_OUTPUT_HEIGHT 1080

/* Muxer batch formation timeout, for e.g. 40 millisec. Should ideally be set
 * based on the fastest source's framerate. */
#define MUXER_BATCH_TIMEOUT_USEC 40000
  • フレームのカウンタと、物体のクラス名を定義しています。gintgchar は GLib の型で、それぞれC言語の int と char に対応します。
gint frame_number = 0;
gchar pgie_classes_str[4][32] = { "Vehicle", "TwoWheeler", "Person",
  "Roadsign"
};
  • osd_sink_pad_buffer_probe()プローブと呼ばれるコールバックです。後述する設定で、nvdsosd に推論結果が入力されるたびに呼び出されるようになっています。
/* osd_sink_pad_buffer_probe  will extract metadata received on OSD sink pad
 * and update params for drawing rectangle, object information etc. */

static GstPadProbeReturn
osd_sink_pad_buffer_probe (GstPad * pad, GstPadProbeInfo * info,
    gpointer u_data)
  • プローブ内で推論結果を取得しています。NvDsBatchMeta がバッチのメタデータ、NvDsFrameMeta がフレームのメタデータ、NvDsObjectMeta が検出した物体のメタデータです。この例では検出した物体が「乗り物」か「人」か調べてカウントしています。
    NvDsBatchMeta *batch_meta = gst_buffer_get_nvds_batch_meta (buf);

    for (l_frame = batch_meta->frame_meta_list; l_frame != NULL;
      l_frame = l_frame->next) {
        NvDsFrameMeta *frame_meta = (NvDsFrameMeta *) (l_frame->data);
        int offset = 0;
        for (l_obj = frame_meta->obj_meta_list; l_obj != NULL;
                l_obj = l_obj->next) {
            obj_meta = (NvDsObjectMeta *) (l_obj->data);
            if (obj_meta->class_id == PGIE_CLASS_ID_VEHICLE) {
                vehicle_count++;
                num_rects++;
            }
            if (obj_meta->class_id == PGIE_CLASS_ID_PERSON) {
                person_count++;
                num_rects++;
            }
        }
  • 次で、カウントした値を nvdsosd で表示するための設定をしています。
        display_meta = nvds_acquire_display_meta_from_pool(batch_meta);
        NvOSD_TextParams *txt_params  = &display_meta->text_params[0];
        display_meta->num_labels = 1;
        txt_params->display_text = g_malloc0 (MAX_DISPLAY_LEN);
        offset = snprintf(txt_params->display_text, MAX_DISPLAY_LEN, "Person = %d ", person_count);
        offset = snprintf(txt_params->display_text + offset , MAX_DISPLAY_LEN, "Vehicle = %d ", vehicle_count);

        /* Now set the offsets where the string should appear */
        txt_params->x_offset = 10;
        txt_params->y_offset = 12;

        /* Font , font-color and font-size */
        txt_params->font_params.font_name = "Serif";
        txt_params->font_params.font_size = 10;
        txt_params->font_params.font_color.red = 1.0;
        txt_params->font_params.font_color.green = 1.0;
        txt_params->font_params.font_color.blue = 1.0;
        txt_params->font_params.font_color.alpha = 1.0;

        /* Text background color */
        txt_params->set_bg_clr = 1;
        txt_params->text_bg_clr.red = 0.0;
        txt_params->text_bg_clr.green = 0.0;
        txt_params->text_bg_clr.blue = 0.0;
        txt_params->text_bg_clr.alpha = 1.0;

        nvds_add_display_meta_to_frame(frame_meta, display_meta);
  • カウントした情報をコンソールにも出力します。g_print は GLib の関数です。書式など基本的な部分は printf と同様です。
  • GST_PAD_PROBE_OK はプローブの正常な返り値です。

    g_print ("Frame Number = %d Number of objects = %d "
            "Vehicle Count = %d Person Count = %d\n",
            frame_number, num_rects, vehicle_count, person_count);
    frame_number++;
    return GST_PAD_PROBE_OK;
}
  • 以下の bus_call() はメッセージハンドラで、パイプラインスレッドからメッセージがあると呼び出されます。動画が終わったときにメインループを終了させたり、エラーが生じたときにメモリを開放したりしています。
static gboolean
bus_call (GstBus * bus, GstMessage * msg, gpointer data)
{
  GMainLoop *loop = (GMainLoop *) data;
  switch (GST_MESSAGE_TYPE (msg)) {
    case GST_MESSAGE_EOS:
      g_print ("End of stream\n");
      g_main_loop_quit (loop);
      break;
    case GST_MESSAGE_ERROR:{
      gchar *debug;
      GError *error;
      gst_message_parse_error (msg, &error, &debug);
      g_printerr ("ERROR from element %s: %s\n",
          GST_OBJECT_NAME (msg->src), error->message);
      if (debug)
        g_printerr ("Error details: %s\n", debug);
      g_free (debug);
      g_error_free (error);
      g_main_loop_quit (loop);
      break;
    }
    default:
      break;
  }
  return TRUE;
}
  • これ以降は main 関数です。
  • 初めにメインループ、各種エレメント、バス、パッドを宣言しています。
int
main (int argc, char *argv[])
{
  GMainLoop *loop = NULL;
  GstElement *pipeline = NULL, *source = NULL, *h264parser = NULL,
      *decoder = NULL, *streammux = NULL, *sink = NULL, *pgie = NULL, *nvvidconv = NULL,
      *nvosd = NULL;

  GstElement *transform = NULL;
  GstBus *bus = NULL;
  guint bus_watch_id;
  GstPad *osd_sink_pad = NULL;
  • GPUの情報を取得しています。これは、後々 PC か Jetson かでパイプラインの構成を切り替える必要があるためです。
    int current_device = -1;
    cudaGetDevice(&current_device);
    struct cudaDeviceProp prop;
    cudaGetDeviceProperties(&prop, current_device);
  • コマンドライン引数を確認します。実行時に動画ファイルのファイル名が入力される必要があります。
  /* Check input arguments */
  if (argc != 2) {
    g_printerr ("Usage: %s <H264 filename>\n", argv[0]);
    return -1;
  }
  • GStreamer を初期化してメインループを作成します。
  /* Standard GStreamer initialization */
  gst_init (&argc, &argv);
  loop = g_main_loop_new (NULL, FALSE);
  • パイプラインとエレメントを作成します。各エレメントの機能は全体の流れで示したとおりです。
  /* Create gstreamer elements */
  /* Create Pipeline element that will form a connection of other elements */
  pipeline = gst_pipeline_new ("dstest1-pipeline");

  /* Source element for reading from the file */
  source = gst_element_factory_make ("filesrc", "file-source");

  /* Since the data format in the input file is elementary h264 stream,
   * we need a h264parser */
  h264parser = gst_element_factory_make ("h264parse", "h264-parser");

  /* Use nvdec_h264 for hardware accelerated decode on GPU */
  decoder = gst_element_factory_make ("nvv4l2decoder", "nvv4l2-decoder");

  /* Create nvstreammux instance to form batches from one or more sources. */
  streammux = gst_element_factory_make ("nvstreammux", "stream-muxer");

  if (!pipeline || !streammux) {
    g_printerr ("One element could not be created. Exiting.\n");
    return -1;
  }

  /* Use nvinfer to run inferencing on decoder's output,
   * behaviour of inferencing is set through config file */
  pgie = gst_element_factory_make ("nvinfer", "primary-nvinference-engine");

  /* Use convertor to convert from NV12 to RGBA as required by nvosd */
  nvvidconv = gst_element_factory_make ("nvvideoconvert", "nvvideo-converter");

  /* Create OSD to draw on the converted RGBA buffer */
  nvosd = gst_element_factory_make ("nvdsosd", "nv-onscreendisplay");

  /* Finally render the osd output */
  if(prop.integrated) {
    transform = gst_element_factory_make ("nvegltransform", "nvegl-transform");
  }
  sink = gst_element_factory_make ("nveglglessink", "nvvideo-renderer");

  if (!source || !h264parser || !decoder || !pgie
      || !nvvidconv || !nvosd || !sink) {
    g_printerr ("One element could not be created. Exiting.\n");
    return -1;
  }

  if(!transform && prop.integrated) {
    g_printerr ("One tegra element could not be created. Exiting.\n");
    return -1;
  }
  • エレメントに必要なプロパティを設定します。
  • 推論の設定ファイルもここで設定します。設定ファイルの書式については Gst-nvinfer File Configuration Specifications を参照してください。
  /* we set the input filename to the source element */
  g_object_set (G_OBJECT (source), "location", argv[1], NULL);

  g_object_set (G_OBJECT (streammux), "batch-size", 1, NULL);

  g_object_set (G_OBJECT (streammux), "width", MUXER_OUTPUT_WIDTH, "height",
      MUXER_OUTPUT_HEIGHT,
      "batched-push-timeout", MUXER_BATCH_TIMEOUT_USEC, NULL);

  /* Set all the necessary properties of the nvinfer element,
   * the necessary ones are : */
  g_object_set (G_OBJECT (pgie),
      "config-file-path", "dstest1_pgie_config.txt", NULL);
  • 上で定義したメッセージハンドラをパイプラインに追加します。
  /* we add a message handler */
  bus = gst_pipeline_get_bus (GST_PIPELINE (pipeline));
  bus_watch_id = gst_bus_add_watch (bus, bus_call, loop);
  gst_object_unref (bus);
  • パイプラインにエレメントを追加します。prop.integrated で Jetson か否かを判断し、必要なエレメントを追加しています。
  • なお、追加しただけではエレメント同士は繋がっていないので、後でリンクする必要があります。
  /* Set up the pipeline */
  /* we add all elements into the pipeline */
  if(prop.integrated) {
    gst_bin_add_many (GST_BIN (pipeline),
        source, h264parser, decoder, streammux, pgie,
        nvvidconv, nvosd, transform, sink, NULL);
  }
  else {
  gst_bin_add_many (GST_BIN (pipeline),
      source, h264parser, decoder, streammux, pgie,
      nvvidconv, nvosd, sink, NULL);
  }
  • decoderstreammux をリンクします。streammux は複数入力に対応しているため、必要な数だけパッドを取得して上流側のパッドとリンクする必要があります。
  GstPad *sinkpad, *srcpad;
  gchar pad_name_sink[16] = "sink_0";
  gchar pad_name_src[16] = "src";

  sinkpad = gst_element_get_request_pad (streammux, pad_name_sink);
  if (!sinkpad) {
    g_printerr ("Streammux request sink pad failed. Exiting.\n");
    return -1;
  }

  srcpad = gst_element_get_static_pad (decoder, pad_name_src);
  if (!srcpad) {
    g_printerr ("Decoder request src pad failed. Exiting.\n");
    return -1;
  }

  if (gst_pad_link (srcpad, sinkpad) != GST_PAD_LINK_OK) {
      g_printerr ("Failed to link decoder to stream muxer. Exiting.\n");
      return -1;
  }

  gst_object_unref (sinkpad);
  gst_object_unref (srcpad);
  • 残りのエレメントをリンクします。streammux と異なりパッドを指定する必要はなく、gst_element_link_many() に並べて入力すればリンクされます。
  /* we link the elements together */
  /* file-source -> h264-parser -> nvh264-decoder ->
   * nvinfer -> nvvidconv -> nvosd -> video-renderer */

  if (!gst_element_link_many (source, h264parser, decoder, NULL)) {
    g_printerr ("Elements could not be linked: 1. Exiting.\n");
    return -1;
  }

  if(prop.integrated) {
    if (!gst_element_link_many (streammux, pgie,
        nvvidconv, nvosd, transform, sink, NULL)) {
      g_printerr ("Elements could not be linked: 2. Exiting.\n");
      return -1;
    }
  }
  else {
    if (!gst_element_link_many (streammux, pgie,
        nvvidconv, nvosd, sink, NULL)) {
      g_printerr ("Elements could not be linked: 2. Exiting.\n");
      return -1;
    }
  }
  • OSD のパッドに、上で定義したプローブを追加します。
  /* Lets add probe to get informed of the meta data generated, we add probe to
   * the sink pad of the osd element, since by that time, the buffer would have
   * had got all the metadata. */
  osd_sink_pad = gst_element_get_static_pad (nvosd, "sink");
  if (!osd_sink_pad)
    g_print ("Unable to get sink pad\n");
  else
    gst_pad_add_probe (osd_sink_pad, GST_PAD_PROBE_TYPE_BUFFER,
        osd_sink_pad_buffer_probe, NULL, NULL);
  gst_object_unref (osd_sink_pad);
  • 準備ができたので、gst_element_set_state() でパイプラインを再生中状態にします。
  • g_main_loop_run() で動画が終了するか中断されるまで待ちます。
  /* Set the pipeline to "playing" state */
  g_print ("Now playing: %s\n", argv[1]);
  gst_element_set_state (pipeline, GST_STATE_PLAYING);

  /* Wait till pipeline encounters an error or EOS */
  g_print ("Running...\n");
  g_main_loop_run (loop);
  • 最後に終了処理を行います。
  /* Out of the main loop, clean up nicely */
  g_print ("Returned, stopping playback\n");
  gst_element_set_state (pipeline, GST_STATE_NULL);
  g_print ("Deleting pipeline\n");
  gst_object_unref (GST_OBJECT (pipeline));
  g_source_remove (bus_watch_id);
  g_main_loop_unref (loop);
  return 0;
}

動作確認

  • ビルドして動作を確認します。
  • 環境変数 CUDA_VER を設定する必要があります。また、サンプルはroot権限のディレクトリにあるので次のようなコマンドでビルドしました。

    $ sudo CUDA_VER=10.2 make
    
  • 実行すると動画が再生され、同時に推論結果が表示されました。

   $ ./deepstream-test1-app ../../../../samples/streams/sample_720p.h264


Web カメラから再生できるように変更する

  • サンプルを実行しただけでは面白くないので、Web カメラの画像を再生できるように変更してみます。
  • 使用するのは ELECOM の古い Web カメラ (UCAM-C0220FBNBK) です。h.264 エンコードには対応しておらず、出力される映像は YUY2 フォーマットです。
  • 参考: realtime v4l2src for deepstream test1 c application does not work

全体の流れ

  • deepstream-test1 の全体の流れとしては、次のようになっています。
    1. v4l2src: v4l2 デバイスからデータを読み込みます。
    2. capsfilter: データのフォーマットを指定します。ここでは YUY2 になります。
    3. nvvideoconvert: YUY2 フォーマットのデータを NV12 フォーマットの NVMM バッファーに変換します。
    4. capsfiter: 再びデータのフォーマットを指定します。ここでは NV12 になります。
    5. nvstreammux: これ以降は上記のサンプルと同様です。

変更点

  • filesrc の代わりに v4l2src を使用します。
  • nvv4l2decoder の代わりに nvvideoconvert を使用します。
  • フォーマットの指定にはケイパビリティという仕組みを使います。(参考: Pads and capabilities)
  • ライブ映像になるため streammux の live-source を設定する必要があります。(参考: Gst-nvstreammux)

以上の変更を適用すると、次のように書き換えられます。

  GMainLoop *loop = NULL;
  GstElement *pipeline = NULL, *source = NULL, *filter1 = NULL, *converter = NULL, *filter2 = NULL,
      *streammux = NULL, *sink = NULL, *pgie = NULL, *nvvidconv = NULL,
      *nvosd = NULL;

  GstElement *transform = NULL;
  GstBus *bus = NULL;
  guint bus_watch_id;
  GstPad *osd_sink_pad = NULL;
  GstCaps *caps;

  int current_device = -1;
  cudaGetDevice(&current_device);
  struct cudaDeviceProp prop;
  cudaGetDeviceProperties(&prop, current_device);
  gst_init (&argc, &argv);
  loop = g_main_loop_new (NULL, FALSE);

  /* Create gstreamer elements */
  /* Create Pipeline element that will form a connection of other elements */
  pipeline = gst_pipeline_new ("dstest1-pipeline");

  /* Source element for reading from the file */
  source = gst_element_factory_make ("v4l2src", "v4l2-source");
  filter1 = gst_element_factory_make ("capsfilter", "v4l2-filter");

  /* Convert to NV12 format */
  converter = gst_element_factory_make ("nvvideoconvert", "nv12-converter");
  filter2 = gst_element_factory_make ("capsfilter", "nv12-filter");

  /* Create nvstreammux instance to form batches from one or more sources. */
  streammux = gst_element_factory_make ("nvstreammux", "stream-muxer");

  if (!pipeline || !streammux) {
    g_printerr ("One element could not be created. Exiting.\n");
    return -1;
  }

  // Create capability
  caps = gst_caps_new_simple (
      "video/x-raw",
      "format", G_TYPE_STRING, "YUY2",
      "width", G_TYPE_INT, 640,
      "height", G_TYPE_INT, 480, NULL);
  g_object_set (G_OBJECT(filter1), "caps", caps, NULL);

  gchar *string1 = "video/x-raw(memory:NVMM),format=(string)NV12";
  caps = gst_caps_from_string(string1);
  g_object_set (G_OBJECT(filter2), "caps", caps, NULL);

  /* Use nvinfer to run inferencing on decoder's output,
   * behaviour of inferencing is set through config file */
  pgie = gst_element_factory_make ("nvinfer", "primary-nvinference-engine");

  /* Use convertor to convert from NV12 to RGBA as required by nvosd */
  nvvidconv = gst_element_factory_make ("nvvideoconvert", "nvvideo-converter");

  /* Create OSD to draw on the converted RGBA buffer */
  nvosd = gst_element_factory_make ("nvdsosd", "nv-onscreendisplay");

  /* Finally render the osd output */
  if(prop.integrated) {
    transform = gst_element_factory_make ("nvegltransform", "nvegl-transform");
  }
  sink = gst_element_factory_make ("nveglglessink", "nvvideo-renderer");

  if (!source || !filter1 || !converter || !filter2 || !pgie
      || !nvvidconv || !nvosd || !sink) {
    g_printerr ("One element could not be created. Exiting.\n");
    return -1;
  }

  if(!transform && prop.integrated) {
    g_printerr ("One tegra element could not be created. Exiting.\n");
    return -1;
  }

  /* we set the input filename to the source element */
  // g_object_set (G_OBJECT (source), "location", argv[1], NULL);

  g_object_set (G_OBJECT (streammux), "batch-size", 1, NULL);

  g_object_set (G_OBJECT (streammux), "width", MUXER_OUTPUT_WIDTH, "height",
      MUXER_OUTPUT_HEIGHT,
      "batched-push-timeout", MUXER_BATCH_TIMEOUT_USEC,
      "live-source", 1, NULL);

動作確認

  • ビルドして実行すると、カメラの映像と推論結果が出力されました。

    $ ./deepstream-test1-app
    


おわりに

DeepStream の C/C++ サンプルを動かして、利用方法を学ぶことができました。最初は GStreamer も動画のフォーマットもよく分からない状態から始めたのでなかなか大変でした。

DeepStream には物体検出だけでなく、トラッキング、クラウドとの連携、転移学習、その他多くの機能があるらしいので、使いこなしていきたいところです。

18
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
11