Repository: open-mmlab/mmocr

Issue:
Allow a way to specify configs dir globally
User should be able to either provide the various functions/classes with a `config_dir`, or use an `export MMOCR_CONFIG_DIR`, or something like `mmocr.register_config_dir(path: str)`.

I would then grab all the configs and put them in `home/.mmocr_configs` and not have to worry about putting it in my projects.

Another use case might be that the project already has a `configs` folder used for another purpose and the user wishes to have mmocr configs in `mmocr_configs` instead.

If a user wishes to have a separate config folder for their own custom configs, in those cases they can explicitly specify it on a use-by-use basis.

**Motivation**
Working with the mmocr (or mmdet for that matter) in multiple projects can be a pain as one needs to copy the config into their project folder over and over. Moreover, it's not clear that it makes sense to use the name `configs` for the mmocr configs.

**Additional context**

One example is with `mmocr.utils.ocr.MMOCR`. There you can provide a `det_config` and a `recog_config` but you always have to change it depending on your `det` and `recog`. Could make it that a `config_dir` is provided without having to specify the exact file. But I think the env variable or global registration would be better.


Discussion:
First, thanks for raising this issue! However, I am quite confused - It seems that you want MMOCR to find configs given the path to the config folder, but which config file would the user want if there are more than 2 configs in the folder? If you simply do not want to put your config files everywhere, passing their absolution paths to MMOCR would also work. 
Let me know if I have misunderstood your point.
@gaotongxiao Thanks for picking this up. So here's the use case:

I'm trying out various detection and recognition algorithms like this:
```
from mmocr.utils.ocr import MMOCR

ocr = MMOCR(det='DB_r50', recog='SAR')
```
And I am doing this in multiple projects because yesterday I was working with one client, and today I am working with another client.

Now MMOCR does [this](https://github.com/open-mmlab/mmocr/blob/a2af2b4d310bcfc6a6e7cfa934f23211a76ef4ee/mmocr/utils/ocr.py#L406-L408) to find the configs:
```
dir_path = str(Path.cwd())
if not recog_config:
    recog_config = dir_path + '/configs/textrecog/' + \
        textrecog_models[self.tr]['config']
```

So by default it looks at my current working directory. This is the part that I find not ideal. Because it means:

1. I need to copy configs into each of my projects
2. I must call the folder "configs" and it must be in my working dir. This doesn't make sense for my project in most cases.

Your suggestion of passing the absolute paths like this works:

```
ocr = MMOCR(
    det='DB_r50', det_config='/home/alexander/Projects/mmocr/configs/textdet/dbnet/dbnet_r50dcnv2_fpnc_1200e_icdar2015.py',
    recog='SAR', recog_config='/home/alexander/Projects/mmocr/configs/textrecog/sar/sar_r31_parallel_decoder_academic.py',
    device=Config.device)
```

But isn't ideal for experimentation (and not very beginner friendly) as I need to keep changing the config paths each time I want to test a different detection model or recognition model. It's also a bit redundant, as MMOCR should know the relative path (given a configs dir) to the default config for each model.
Okay, so I guess something like
```
ocr = MMOCR(det='DB_r50', recog='SAR', config_dir='/home/alexander/Projects/mmocr/configs/')
```
is what you want? And users can test different models by just changing the names instead of lengthy paths.
@gaotongxiao yep pretty much. Or the other alternatives are:

- `export MMOCR_CFG_DIR=/home/alexander/Projects/mmocr/configs/`
- `mmocr.set_config_dir('/home/alexander/Projects/mmocr/configs/'`)

The goal is simply to avoid being stuck with the assumption that `dir_path = str(Path.cwd())` in the code.

Resolving patch:
diff --git a/mmocr/utils/ocr.py b/mmocr/utils/ocr.py
--- a/mmocr/utils/ocr.py
+++ b/mmocr/utils/ocr.py
@@ -1,3 +1,5 @@
+import os
+import warnings
 from argparse import ArgumentParser, Namespace
 from pathlib import Path
 
@@ -8,6 +10,7 @@
 from mmocr.apis.inference import model_inference
 from mmocr.core.visualize import det_recog_show_result
 from mmocr.datasets.pipelines.crop import crop_img
+from mmocr.utils.box_util import stitch_boxes_into_lines
 
 textdet_models = {
     'DB_r18': {
@@ -195,6 +198,10 @@ def det_and_recog_inference(args, det_model, recog_model):
                 img_e2e_res['result'][i]['text'] = text
                 img_e2e_res['result'][i]['text_score'] = text_score
 
+        if args.merge:
+            img_e2e_res['result'] = stitch_boxes_into_lines(
+                img_e2e_res['result'], args.merge_xdist, 0.5)
+
         end2end_res.append(img_e2e_res)
     return end2end_res
 
@@ -296,19 +303,42 @@ def parse_args():
         '--det',
         type=str,
         default='PANet_IC15',
-        help='Text detection algorithm')
+        help='Pretrained text detection algorithm')
     parser.add_argument(
         '--det-config',
         type=str,
         default='',
-        help='Path to the custom config of the selected det model')
+        help='Path to the custom config file of the selected det model. It '
+        'overrides the settings in det')
     parser.add_argument(
-        '--recog', type=str, default='SEG', help='Text recognition algorithm')
+        '--det-ckpt',
+        type=str,
+        default='',
+        help='Path to the custom checkpoint file of the selected det model. '
+        'It overrides the settings in det')
+    parser.add_argument(
+        '--recog',
+        type=str,
+        default='SEG',
+        help='Pretrained text recognition algorithm')
     parser.add_argument(
         '--recog-config',
         type=str,
         default='',
-        help='Path to the custom config of the selected recog model')
+        help='Path to the custom config file of the selected recog model. It'
+        'overrides the settings in recog')
+    parser.add_argument(
+        '--recog-ckpt',
+        type=str,
+        default='',
+        help='Path to the custom checkpoint file of the selected recog model. '
+        'It overrides the settings in recog')
+    parser.add_argument(
+        '--config-dir',
+        type=str,
+        default=os.path.join(str(Path.cwd()), 'configs/'),
+        help='Path to the config directory where all the config files '
+        'are located. Defaults to "configs/"')
     parser.add_argument(
         '--batch-mode',
         action='store_true',
@@ -353,11 +383,29 @@ def parse_args():
         '--print-result',
         action='store_true',
         help='Prints the recognised text')
+    parser.add_argument(
+        '--merge', action='store_true', help='Merge neighboring boxes')
+    parser.add_argument(
+        '--merge-xdist',
+        type=float,
+        default=20,
+        help='The maximum x-axis distance to merge boxes')
     args = parser.parse_args()
     if args.det == 'None':
         args.det = None
     if args.recog == 'None':
         args.recog = None
+    # Warnings
+    if args.merge and not (args.det and args.recog):
+        warnings.warn(
+            'Box merging will not work if the script is not'
+            ' running in detection + recognition mode.', UserWarning)
+    if not os.path.samefile(args.config_dir, os.path.join(str(
+            Path.cwd()))) and (args.det_config != ''
+                               or args.recog_config != ''):
+        warnings.warn(
+            'config_dir will be overrided by det-config or recog-config.',
+            UserWarning)
     return args
 
 
@@ -366,16 +414,16 @@ class MMOCR:
     def __init__(self,
                  det='PANet_IC15',
                  det_config='',
+                 det_ckpt='',
                  recog='SEG',
                  recog_config='',
+                 recog_ckpt='',
+                 config_dir=os.path.join(str(Path.cwd()), 'configs/'),
                  device='cuda:0',
                  **kwargs):
         self.td = det
         self.tr = recog
-        if device == 'cpu':
-            self.device = 0
-        else:
-            self.device = device
+        self.device = device
 
         # Check if the det/recog model choice is valid
         if self.td and self.td not in textdet_models:
@@ -385,16 +433,14 @@ def __init__(self,
             raise ValueError(self.tr,
                              'is not a supported text recognition algorithm')
 
-        # By default, the config folder should be in the cwd
-        dir_path = str(Path.cwd())
-
         if self.td:
             # Build detection model
             if not det_config:
-                det_config = dir_path + '/configs/textdet/' + textdet_models[
-                    self.td]['config']
-            det_ckpt = 'https://download.openmmlab.com/mmocr/textdet/' + \
-                textdet_models[self.td]['ckpt']
+                det_config = os.path.join(config_dir, 'textdet/',
+                                          textdet_models[self.td]['config'])
+            if not det_ckpt:
+                det_ckpt = 'https://download.openmmlab.com/mmocr/textdet/' + \
+                    textdet_models[self.td]['ckpt']
 
             self.detect_model = init_detector(
                 det_config, det_ckpt, device=self.device)
@@ -404,10 +450,12 @@ def __init__(self,
         if self.tr:
             # Build recognition model
             if not recog_config:
-                recog_config = dir_path + '/configs/textrecog/' + \
-                    textrecog_models[self.tr]['config']
-            recog_ckpt = 'https://download.openmmlab.com/mmocr/textrecog/' + \
-                textrecog_models[self.tr]['ckpt']
+                recog_config = os.path.join(
+                    config_dir, 'textrecog/',
+                    textrecog_models[self.tr]['config'])
+            if not recog_ckpt:
+                recog_ckpt = 'https://download.openmmlab.com/mmocr/'
+                'textrecog/' + textrecog_models[self.tr]['ckpt']
 
             self.recog_model = init_detector(
                 recog_config, recog_ckpt, device=self.device)
@@ -434,6 +482,8 @@ def readtext(self,
                  single_batch_size=0,
                  imshow=False,
                  print_result=False,
+                 merge=False,
+                 merge_xdist=20,
                  **kwargs):
         args = locals()
         [args.pop(x, None) for x in ['kwargs', 'self']]


Test patch: