【やってみた】YOLOv8の機能試す&Webカメラでリアルタイム推論
宣伝
こんなコミュニティもやっているので良ければご参加ください!
connpassはこちらから!
はじめに
今回は最近登場した話題のYOLOv8をわかる範囲でしゃぶりつくします。
ところでYOLOv8ってすごい数まで来ましたね。つい1年前くらいはv5だとか言ってたはずなんですが。
そろそろYOLOって名前じゃなくて、別のアーキテクチャ名つけたほうが良いのでは・・・?🤔
実験環境
今回の実験環境は次の通りです。
- Windows11(12th Gen Intel(R) Core(TM) i5-12400F 2.50 GHz)
- conda
もしconda環境がまだない方は
を参考に導入お願いします。
まずはYOLOv8の準備
何はともあれYOLOv8の準備をしましょう。
git clone https://github.com/ultralytics/ultralytics
そして必要そうなものをインストール
pip install -r requirements.txt
公式ではpipでのインストールを勧められていますが、今回はお試しなのでリポジトリをクローンします。
YOLOv8を呼び出して推論する
公式のチュートリアルに従って呼び出しましょう。
from ultralytics import YOLO
model = YOLO("yolov8n.pt")
続いて推論してみましょう。
results = model("https://ultralytics.com/images/bus.jpg")
results
#<generator object BasePredictor.__call__ at 0x00000266C4DA3EB0>
・・・。これは・・・、なんだ・・・?
詳しく扱えるようにいじってみましょう。
modelから手がかりを得る
まずはmodel
が何で構成されているか確認します。
type(model)
#ultralytics.yolo.engine.model.YOLO
なるほど、model
はultralytics.yolo.engine.model.YOLO
で構成されているようです。
該当のスクリプトを確認してみましょう。
class YOLO
"""
YOLO
A python interface which emulates a model-like behaviour by wrapping trainers.
"""
def __init__(self, model='yolov8n.yaml', type="v8") -> None:
"""
> Initializes the YOLO object.
Args:
model (str, Path): model to load or create
type (str): Type/version of models to use. Defaults to "v8".
"""
self.type = type
self.ModelClass = None # model class
self.TrainerClass = None # trainer class
self.ValidatorClass = None # validator class
self.PredictorClass = None # predictor class
self.model = None # model object
self.trainer = None # trainer object
self.task = None # task type
self.ckpt = None # if loaded from *.pt
self.cfg = None # if loaded from *.yaml
self.ckpt_path = None
self.overrides = {} # overrides for trainer object
# Load or create new YOLO model
{'.pt': self._load, '.yaml': self._new}[Path(model).suffix](model)
def __call__(self, source, **kwargs):
return self.predict(source, **kwargs)
def _new(self, cfg: str, verbose=True):
"""
> Initializes a new model and infers the task type from the model definitions.
Args:
cfg (str): model configuration file
verbose (bool): display model info on load
"""
cfg = check_yaml(cfg) # check YAML
cfg_dict = yaml_load(cfg, append_filename=True) # model dict
self.task = guess_task_from_head(cfg_dict["head"][-1][-2])
self.ModelClass, self.TrainerClass, self.ValidatorClass, self.PredictorClass = \
self._guess_ops_from_task(self.task)
self.model = self.ModelClass(cfg_dict, verbose=verbose) # initialize
self.cfg = cfg
def _load(self, weights: str):
"""
> Initializes a new model and infers the task type from the model head.
Args:
weights (str): model checkpoint to be loaded
"""
self.model, self.ckpt = attempt_load_one_weight(weights)
self.ckpt_path = weights
self.task = self.model.args["task"]
self.overrides = self.model.args
self._reset_ckpt_args(self.overrides)
self.ModelClass, self.TrainerClass, self.ValidatorClass, self.PredictorClass = \
self._guess_ops_from_task(self.task)
def reset(self):
"""
> Resets the model modules.
"""
for m in self.model.modules():
if hasattr(m, 'reset_parameters'):
m.reset_parameters()
for p in self.model.parameters():
p.requires_grad = True
def info(self, verbose=False):
"""
> Logs model info.
Args:
verbose (bool): Controls verbosity.
"""
self.model.info(verbose=verbose)
def fuse(self):
self.model.fuse()
@smart_inference_mode()
def predict(self, source, return_outputs=True, **kwargs):
"""
Visualize prediction.
Args:
source (str): Accepts all source types accepted by yolo
**kwargs : Any other args accepted by the predictors. To see all args check 'configuration' section in docs
"""
overrides = self.overrides.copy()
overrides["conf"] = 0.25
overrides.update(kwargs)
overrides["mode"] = "predict"
overrides["save"] = kwargs.get("save", False) # not save files by default
predictor = self.PredictorClass(overrides=overrides)
predictor.args.imgsz = check_imgsz(predictor.args.imgsz, min_dim=2) # check image size
predictor.setup(model=self.model, source=source, return_outputs=return_outputs)
return predictor() if return_outputs else predictor.predict_cli()
@smart_inference_mode()
def val(self, data=None, **kwargs):
"""
> Validate a model on a given dataset .
Args:
data (str): The dataset to validate on. Accepts all formats accepted by yolo
**kwargs : Any other args accepted by the validators. To see all args check 'configuration' section in docs
"""
overrides = self.overrides.copy()
overrides.update(kwargs)
overrides["mode"] = "val"
args = get_config(config=DEFAULT_CONFIG, overrides=overrides)
args.data = data or args.data
args.task = self.task
validator = self.ValidatorClass(args=args)
validator(model=self.model)
@smart_inference_mode()
def export(self, **kwargs):
"""
> Export model.
Args:
**kwargs : Any other args accepted by the predictors. To see all args check 'configuration' section in docs
"""
overrides = self.overrides.copy()
overrides.update(kwargs)
args = get_config(config=DEFAULT_CONFIG, overrides=overrides)
args.task = self.task
exporter = Exporter(overrides=args)
exporter(model=self.model)
def train(self, **kwargs):
"""
> Trains the model on a given dataset.
Args:
**kwargs (Any): Any number of arguments representing the training configuration. List of all args can be found in 'config' section.
You can pass all arguments as a yaml file in `cfg`. Other args are ignored if `cfg` file is passed
"""
overrides = self.overrides.copy()
overrides.update(kwargs)
if kwargs.get("cfg"):
LOGGER.info(f"cfg file passed. Overriding default params with {kwargs['cfg']}.")
overrides = yaml_load(check_yaml(kwargs["cfg"]), append_filename=True)
overrides["task"] = self.task
overrides["mode"] = "train"
if not overrides.get("data"):
raise AttributeError("dataset not provided! Please define `data` in config.yaml or pass as an argument.")
if overrides.get("resume"):
overrides["resume"] = self.ckpt_path
self.trainer = self.TrainerClass(overrides=overrides)
if not overrides.get("resume"): # manually set model only if not resuming
self.trainer.model = self.trainer.get_model(weights=self.model if self.ckpt else None, cfg=self.model.yaml)
self.model = self.trainer.model
self.trainer.train()
def to(self, device):
"""
> Sends the model to the given device.
Args:
device (str): device
"""
self.model.to(device)
def _guess_ops_from_task(self, task):
model_class, train_lit, val_lit, pred_lit = MODEL_MAP[task]
# warning: eval is unsafe. Use with caution
trainer_class = eval(train_lit.replace("TYPE", f"{self.type}"))
validator_class = eval(val_lit.replace("TYPE", f"{self.type}"))
predictor_class = eval(pred_lit.replace("TYPE", f"{self.type}"))
return model_class, trainer_class, validator_class, predictor_class
@staticmethod
def _reset_ckpt_args(args):
args.pop("project", None)
args.pop("name", None)
args.pop("batch", None)
args.pop("epochs", None)
args.pop("cache", None)
args.pop("save_json", None)
# set device to '' to prevent from auto DDP usage
args["device"] = ''
この中で推論時に使っていそうな関数は、predict
関数ですね。正しいかどうか確認するために以下のようにして実験してみました。
def predict(self, source, return_outputs=True, **kwargs):
"""
Visualize prediction.
Args:
source (str): Accepts all source types accepted by yolo
**kwargs : Any other args accepted by the predictors. To see all args check 'configuration' section in docs
"""
print("これで推論しているよ")
overrides = self.overrides.copy()
overrides["conf"] = 0.25
overrides.update(kwargs)
overrides["mode"] = "predict"
overrides["save"] = kwargs.get("save", False) # not save files by default
predictor = self.PredictorClass(overrides=overrides)
predictor.args.imgsz = check_imgsz(predictor.args.imgsz, min_dim=2) # check image size
predictor.setup(model=self.model, source=source, return_outputs=return_outputs)
return predictor() if return_outputs else predictor.predict_cli()
以下を実行してみます。
from ultralytics import YOLO
model = YOLO("yolov8n.pt")
results = model("https://ultralytics.com/images/bus.jpg")
#Found https://ultralytics.com/images/bus.jpg locally at bus.jpg
#Ultralytics YOLOv8.0.4 Python-3.9.15 torch-1.13.1+cpu CPU
#Fusing layers...
#YOLOv8n summary: 168 layers, 3151904 parameters, 0 gradients, 8.7 GFLOPs
#これで推論しているよ
あたりですね!ではここを深堀してみましょう。
predict関数を深堀る
def predict(self, source, return_outputs=True, **kwargs):
"""
Visualize prediction.
Args:
source (str): Accepts all source types accepted by yolo
**kwargs : Any other args accepted by the predictors. To see all args check 'configuration' section in docs
"""
overrides = self.overrides.copy()
overrides["conf"] = 0.25
overrides.update(kwargs)
overrides["mode"] = "predict"
overrides["save"] = kwargs.get("save", False) # not save files by default
predictor = self.PredictorClass(overrides=overrides)
predictor.args.imgsz = check_imgsz(predictor.args.imgsz, min_dim=2) # check image size
predictor.setup(model=self.model, source=source, return_outputs=return_outputs)
return predictor() if return_outputs else predictor.predict_cli()
まずここから分かることは
- 強制的にconf=0.25にされている
- 予測には
self.PredictorClass
を利用している - 詳しい引数はドキュメントに書いてある
- 返り値は
predictor()
である
です。意外と面倒ですね。
まずは楽そうなドキュメントを確認してみましょう。
ドキュメントの確認
コメントによると
Any other args accepted by the predictors. To see all args check 'configuration' section in docs
とあります。とりあえずdocs/config.md
を見てみましょう。
うーん英語、でもだいたいわかりそうです。
Key | Value | Description |
---|---|---|
source | ultralytics/assets |
入力には画像、フォルダ、ビデオ、URLが利用可能 |
show | False |
画像を表示する |
save_txt | False |
結果をtxtに保存 |
save_conf | False |
信頼度を保存 |
save_crop | Fasle |
検出結果をクロップして保存 |
hide_labels | False |
ラベルを消す |
hide_conf | False |
信頼度を消す |
vid_stride | False |
フレームを飛ばす?(要調査) |
line_thickness | 3 |
bbox描画の太さを制御 |
visualize | False |
モデルの特徴を表現 |
augment | False |
拡張推論を実行 |
agnostic_nms | False |
クラスに依存しないNMSを利用する |
retina_masks | False |
超解像でマスクを生成(Segmentationのみ) |
引数で渡すとうまく動きそうですね。少し推論を試してみましょう。
soureceの扱い
sourceは次のが扱えるようです。
- 画像までのパス
- フォルダ
- URL
- Webカメラ
- スクリーンショット
まずはフォルダのultralytics/assets
を利用してみます。
from ultralytics import YOLO
model = YOLO("yolov8n.pt")
for i in results:
print(i)
# image 1/2 C:\Users\OPamp\Desktop\Python_experiment\ultralytics\ultralytics\assets\bus.jpg: 640x480 4 persons, 1 bus, 1 stop sign, 71.6ms
# image 2/2 C:\Users\OPamp\Desktop\Python_experiment\ultralytics\ultralytics\assets\zidane.jpg: 384x640 2 persons, 1 tie, 53.2ms
# Speed: 0.5ms pre-process, 62.4ms inference, 1.4ms postprocess per image at shape (1, 3, 640, 640)
# {'det': array([[ 17, 231, 802, 768, 0.87055, 5],
# [ 49, 399, 245, 903, 0.86898, 0],
# [ 670, 380, 810, 876, 0.8536, 0],
# [ 221, 406, 345, 857, 0.81931, 0],
# [ 0, 255, 32, 325, 0.34607, 11],
# [ 0, 551, 67, 874, 0.30129, 0]], dtype=float32)}
# {'det': array([[ 123, 197, 1111, 711, 0.80557, 0],
# [ 747, 41, 1142, 712, 0.79367, 0],
# [ 437, 437, 524, 714, 0.37022, 27]], dtype=float32)}
検出結果をこんな感じで受け取れるようです。
また、引数にsave=True
を追記すると、runs/detect/predict
に結果が保存されます。
showの扱い
続いてshowの扱いについて。こちらは
def show(self, p):
im0 = self.annotator.result()
if platform.system() == 'Linux' and p not in self.windows:
self.windows.append(p)
cv2.namedWindow(str(p), cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO) # allow window resize (Linux)
cv2.resizeWindow(str(p), im0.shape[1], im0.shape[0])
cv2.imshow(str(p), im0)
cv2.waitKey(1) # 1 millisecond
が利用されているようです。ここが分かった理由としては、self.PredictorClass
がultralytics.yolo.v8.detect.predict.DetectionPredictor
であり
class DetectionPredictor(BasePredictor):
def get_annotator(self, img):
return Annotator(img, line_width=self.args.line_thickness, example=str(self.model.names))
となっており、BasePredictorを継承しているところから確認しました。
このshowは動画・Webカメラ・スクリーンショットを利用した際に利用できるようです。
スクリーンショットの場合は
from ultralytics import YOLO
model = YOLO("yolov8n.pt")
results = model("screen" , show=True)
for i in enumerate(results):
print(i)
Webカメラの場合はカメラ番号を入れれば実行可能です。
from ultralytics import YOLO
model = YOLO("yolov8n.pt")
results = model(0 , show=True)
for i in enumerate(results):
print(i)
# 0: 480x640 1 carrot, 1 cake, 1 book, 74.1ms
# runs\detect\predict5\0
# {'det': array([[ 6, 334, 228, 374, 0.58858, 51],
# [ 0, 62, 304, 251, 0.39537, 55],
# [ 354, 111, 640, 274, 0.32949, 73]], dtype=float32)}
# 0: 480x640 1 carrot, 1 cake, 1 book, 70.5ms
# runs\detect\predict5\0
# {'det': array([[ 6, 334, 231, 375, 0.53592, 51],
# [ 0, 62, 304, 251, 0.41555, 55],
# [ 354, 110, 640, 277, 0.36091, 73]], dtype=float32)}
# 0: 480x640 1 carrot, 1 cake, 1 book, 72.5ms
save_txtの扱い
続いてsave_txtの扱いについて見ていきましょう。こちらは実行時に予測結果を保存してくれる関数です。
from ultralytics import YOLO
model = YOLO("yolov8n.pt")
results = model("ultralytics/assets",save_txt=True)
for i in results:
print(i)
0 0.041358 0.659722 0.082716 0.299074
11 0.0197531 0.268519 0.0395062 0.0648148
0 0.349383 0.584722 0.153086 0.417593
0 0.91358 0.581481 0.17284 0.459259
0 0.181481 0.602778 0.241975 0.466667
5 0.505556 0.4625 0.969136 0.497222
save_confの扱い
続いてsave_confの扱いを見ていきましょう。こちらは信頼度を保存してくれる引数です。こちらを利用する際は上のsave_txt=True
を同時に利用してください。上で生成された.txtにconfを追加するようです。
from ultralytics import YOLO
model = YOLO("yolov8n.pt")
results = model("ultralytics/assets",save_txt=True,save_conf=True)
for i in results:
print(i)
0 0.041358 0.659722 0.082716 0.299074 0.301294
11 0.0197531 0.268519 0.0395062 0.0648148 0.346069
0 0.349383 0.584722 0.153086 0.417593 0.819305
0 0.91358 0.581481 0.17284 0.459259 0.853604
0 0.181481 0.602778 0.241975 0.466667 0.86898
5 0.505556 0.4625 0.969136 0.497222 0.870545
save_cropの扱い
続いてsave_cropの扱いを見ていきましょう。こちらを実行すると検出された対象を切り出して保存してくれます。
from ultralytics import YOLO
model = YOLO("yolov8n.pt")
results = model("ultralytics/assets",save_crop=True)
for i in results:
print(i)
hide_labelsの扱い
続いてhide_labelsの扱いを見ていきましょう。こちらを実行すると、検出結果からラベルを消し去ります。
from ultralytics import YOLO
model = YOLO("yolov8n.pt")
results = model("ultralytics/assets",save=True, hide_labels=True)
for i in results:
print(i)
hide_confの扱い
続いてhide_confの扱いを見ていきましょう。こちらを実行すると、信頼度のみを消し去ります。
from ultralytics import YOLO
model = YOLO("yolov8n.pt")
results = model("ultralytics/assets",save=True, hide_conf=True)
for i in results:
print(i)
line_thicknessの扱い
続いてline_thicknessの扱いを見ていきましょう。こちらを変更すると、描画時のラインの太さが変化します。
from ultralytics import YOLO
model = YOLO("yolov8n.pt")
results = model("ultralytics/assets",save=True, line_thickness=1)
for i in results:
print(i)
そのほかの変数
そのほかに指定できる(効くかわからないが)は次の通りです。
{'task': 'detect', 'mode': 'train', 'model': 'yolov8n.yaml', 'data': 'coco.yaml', 'patience': 50, 'imgsz': 640, 'save': True, 'device': '', 'workers': 8, 'exist_ok': False, 'pretrained': False, 'optimizer': 'SGD', 'verbose': False, 'seed': 0, 'deterministic': True, 'single_cls': False, 'image_weights': False, 'rect': False, 'cos_lr': False, 'close_mosaic': 10, 'resume': False, 'overlap_mask': True, 'mask_ratio': 4, 'dropout': False, 'val': True, 'save_hybrid': False, 'conf': 0.25, 'iou': 0.7, 'max_det': 300, 'half': True, 'dnn': False, 'plots': True, 'source': 'ultralytics/assets/', 'show': False, 'save_txt': False, 'save_conf': False, 'save_crop': False, 'hide_labels': False, 'hide_conf': False, 'vid_stride': 1, 'line_thickness': 3, 'visualize': True, 'augment': False, 'agnostic_nms': False, 'retina_masks': False, 'format': 'torchscript', 'keras': False, 'optimize': False, 'int8': False, 'dynamic': False, 'simplify': False, 'opset': 17, 'workspace': 4, 'nms': False, 'lr0': 0.01, 'lrf': 0.01, 'momentum': 0.937, 'weight_decay': 0.001, 'warmup_epochs': 3.0, 'warmup_momentum': 0.8, 'warmup_bias_lr': 0.1, 'box': 7.5, 'cls': 0.5, 'dfl': 1.5, 'fl_gamma': 0.0, 'label_smoothing': 0.0, 'nbs': 64, 'hsv_h': 0.015, 'hsv_s': 0.7, 'hsv_v': 0.4, 'degrees': 0.0, 'translate': 0.1, 'scale': 0.5, 'shear': 0.0, 'perspective': 0.0, 'flipud': 0.0, 'fliplr': 0.5, 'mosaic': 1.0, 'mixup': 0.0, 'copy_paste': 0.0, 'cfg': None, 'hydra': {'output_subdir': None, 'run': {'dir': '.'}}, 'v5loader': True}
特に
imgsz': 640, 'conf': 0.25, 'iou': 0.7, 'max_det': 300, 'half': True,
は推論時に利用されているので変更すると精度が変わります。
例えば、conf=0.001にした場合は
from ultralytics import YOLO
model = YOLO("yolov8n.pt")
results = model("ultralytics/assets",save=True, conf=0.001)
for i in results:
print(i)
となります。(めちゃくちゃだ・・・)
もっと最小限でWebカメラリアルタイム推論がしたい
色々な関数を引っ張り出してきて、以下のようになりました。これでとりあえずコンペなどで利用できるようになりそうですね。
from ultralytics import YOLO
import torch
import cv2
from ultralytics.yolo.data.augment import LetterBox
from ultralytics.yolo.utils.plotting import Annotator, colors
from ultralytics.yolo.utils import ops
from copy import deepcopy
import numpy as np
import matplotlib.pyplot as plt
model = YOLO("yolov8n.pt")
cap = cv2.VideoCapture(0)
def preprocess(img, size=640):
img = LetterBox(size, True)(image=img)
img = img.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB
img = np.ascontiguousarray(img) # contiguous
img = torch.from_numpy(img)
img = img.float() # uint8 to fp16/32
img /= 255 # 0 - 255 to 0.0 - 1.0
return img.unsqueeze(0)
def postprocess(preds, img, orig_img):
preds = ops.non_max_suppression(preds,
0.25,
0.8,
agnostic=False,
max_det=100)
for i, pred in enumerate(preds):
shape = orig_img.shape
pred[:, :4] = ops.scale_boxes(img.shape[2:], pred[:, :4], shape).round()
return preds
def drow_bbox(pred, names, annotator):
for *xyxy, conf, cls in reversed(pred):
c = int(cls) # integer class
label = f'{names[c]} {conf:.2f}'
annotator.box_label(xyxy, label, color=colors(c, True))
while True:
ret, img = cap.read()
origin = deepcopy(img)
annotator = Annotator(origin,line_width=1,example=str(model.model.names))
img = preprocess(img)
preds = model.model(img, augment=False)
preds = postprocess(preds,img,origin)
drow_bbox(preds[0], model.model.names, annotator)
cv2.imshow("test",origin)
cv2.waitKey(1)
まとめ
今回は今はやりのYOLOv8の使い方について調査しました。YOLOv5から大きく進化を遂げており、学習・推論が簡単になっていそうな雰囲気を感じ取りました。
ただ、精度がどうかと言わるとまだコンペで使ってないから・・・。と答えるしか・・・。
とにもかくにも、最新モデルを触れたのでいい経験になりました!
次はYOLOv8の構造の調査とかしたいなぁ。
Discussion
とても参考になりました!ありがとうございます。
質問なのですが、読み込む画像を、ファイルやデスクトップ全体、カメラではなく、mssライブラリを使ってデスクトップの一部分のみの画像を推論させることは可能でしょうか?
ご返信お待ちしております。
コメントありがとうございます。
mssライブラリを使っての推論ですが、これは如何でしょうか?
ありがとうございます。記載ミスしていました。正確には、mssライブラリを使ってリアルタイムでデスクトップ上の画面をキャプチャし、そのデータをyolov8に渡してリアルタイム推論できるようにしたいのです。
以前、自分はYOLOv5を使ってこのように書くことができました。
yolov8で同じようなことをしてもうまくいかなかったので、質問させていただきました。素人質問で申し訳ないです💦
なるほどです!
YOLOv5のような手軽にレンダリングできる感じは今のところは見つけられていません・・・。
代替案として以下はどうでしょうか?
リアルタイムに画面の一部を推論できると思います!
ありがとうございます!感動しました!自分では到底作ることが不可能でした😓 まだコードの意味がわからないので、解読してみます。余談ですが、どのように勉強すればこのようなコードが書けるか、おすすめの勉強法などあったら初心者の自分にアドバイスいただけると嬉しいです🙏
勉強法・・・というほどのことでもないですが、とにかく作りたいと思ったものを頑張って実装するのが近道かと思います。今回の記事も最後には「もっと最小限の機能でリアルタイム推論がしたい」というところを実現するために、ライブラリのあらゆるコードを読んで必要そうな関数を引っ張り出してきていたりします。
こういう、泥臭ーいことをしている時が一番身になったりするんじゃないかなと考えます。
あんまり具体的な方法でなくて申し訳ありません・・・。
こちら、とても参考になりました。ありがとうございます!今後も記事を楽しみにしております!
初めまして!とても参考になりました!質問なのですが、上記のスクリーンショットのリアルタイム推論の例はデフォルトでGPUが選択されているのでしょうか?よろしくお願いします。
たぶんですが、CPUで動いていると思います。ので、
.to("cuda")
でモデルとデータをGPUメモリに乗せてもらうのが確実かと思います。返信ありがとうございます!具体的にどの行を変更すればよいのでしょうか?素人で申し訳ありません。よろしくお願いします。
model = YOLO("yolov8n.pt")
のあとに
model.to("cuda")を追加して、preprocessのimg = torch.from_numpy(img)をimg = torch.from_numpy(img).to("cuda")に変えたら出来ました。
はじめまして。この記事を見て勉強させていただいています。
どうしてもわからないことがあるので質問させてください。
この記事のコードのように
from ultralytics import YOLO
model = YOLO("yolov8n.pt")
results = model(0 , show=True)
for i in enumerate(results):
print(i)
として解析を一旦始めてしまうとどうやっても止めることができなくなります。検索するとqをおすなどが出てくるのですが、止めることができません。止め方をご存知でしたら教えていただけると助かります。