Notes of the calibration of MultivewX_Perception(CalibrateTool)

WHAT IS NEW!!

支持利用数据集文件夹中 calibrations 中的标定数据与 datasetparameters.py 中的 NUM_CAM MAP_HEIGHT MAP_WIDTH OverlapUnitConvert OverlapGridOffset 参数给出其相机姿态和视野范围。

例如,当我们希望为 Wildtrack 数据集产生 Overlap view 时:

在命令行中输入 -view D:\Wildtrack ,注意需保证其calibrations文件夹下有extrinsicintrinsic文件夹(与Wildtrack格式一致)。

运行可得下列结果。

Camera 6

左图为本工具对 Wildtrack 的结果,右图(有一定变形)为参考

注意:请修改上述的五个参数以符合实际情况,MAP 使用的单位应当与其 calibration 使用的单位相同。Wildtrack 数据集工具声明其网格起点与世界原点并非同处,网格起点为(-300,-90,0)cm,似乎有误。此处使用的是网格起点为(-300,-900,0)cm

Args

MultiviewX_Perception可以接受命令行参数,从而用户可以快速高效地产生数据集。当未接收到相关参数时候,不会启用相关功能。

-a :Annotate and show the bbox on the first frame of each camera or not.

-s :Save the bbox on the first frame of each camera or not.

-k :Keep the remains of Perception dataset or not.

-f :Force calibrate and generate POM, regardless of perception.

-p n: Provide preview for the front n frames. ex. -p 5 will provide 5 frames to preview

-v :Generate Overlap view for the dataset

-view path :Generate Overlap view for the specified dataset, there should be folder calibrations in the given path. ex. -view D:\Wildtrack

例如,当只想借助CalibrateTool进行标定时,可以输入-f,程序会跳过处理percetion数据步骤,也不会进行后续标注的环节。

1
python run_all.py -f

Keep in Mind

CalibrateTool 现在是 WildPerception 的标定部分

Github 项目地址:MultiviewX_WildPerception

欢迎下载示例文件:sample.zip

对于场景:

  1. Unity长度(米)➗ Scaling = OpenCV长度(米)

  2. (Unity点坐标 - Unity中GridOrigin的Unity坐标)➗Scaling , 再交换Unity坐标中的y,z分量,可以得到OpenCV下点坐标。

  3. GridOrigin所在位置是OpenCV下的坐标原点

  4. 棋盘会在黄色辅助正方体内随机生成,辅助正方体的边长等于两倍的tRandomTransform,其中心是chessboardGenerateCenter

  5. 场景中全体markpoint_3dchessboardGenerateCenter的坐标加上一些预设的偏移得到。换言之,场景其实只有一份markpoints_3d,其中心为 chessboardGenerateCenter,均匀分布在水平面上。

    注意:Grid辅助线上标注的和辅助点markpoint_3d上标注的值已经是OpenCV下此点的坐标。

    如下图:

    image-20230322202441746

对于每个相机:

  1. 既然整个场景公用一份markpoints_3d,为什么每个相机下都有markpoints_3d.txt这么个文件呢?

    因为不是全体markpoint_3d都在此相机的视野范围内,需要针对每个相机进行剔除对应点。每个相机的 markpoints_3d.txtmarkpoints_2d.txt 共同得出了其外参。

  2. 务必保证Game View下的分辨率与CalibrateTool配置的分辨率相同,否则会直接退出运行并报错。

  3. tRandomTransform的选择推荐是,使得黄色辅助正方体大部分在所有相机的视野中。

Introduction

CalibrateTool是一个在Unity3D中为一个或多个相机,产生多个虚拟的不同角度朝向的棋盘格数据且给出待标定相机对应内外参的工具。其生成的虚拟棋盘数据等效于利用OpenCV中cv.findChessboardCorners所产生的结果。同时,CalibrateTool 可以完成一些运行 MultiviewX_Perception 所需要的设置,诸如设置地图大小、地图格点起始位置等。具体使用在 [Work with MultiviewX_Perception](# Work with MultiviewX_Perception) 标题下。

下列图片为标注环节的效果演示,此环节不在CalibrateTool能力范围内,是MultviewX_Perception的后续环节。此处贴上标注的图仅仅用来说明CalibrateTool的缩放、OpenCV坐标系的设置、标定是合理有效的。

Grid地图

bbox\_cam9

bbox\_cam8

Setup

CalibrateTool.unitypackage 是对应的Unity资产,其中包含了一个带有CalibrateTool组件的预制体和组件对应的代码。(面板可能因为版本不同略有出入,推荐总是使用最新的一个版本)

  1. unitypackage包导入完成后,我们可以将 CalibrateTool 拖入到需要标定的场景中:

    检视面板

  2. 根据自己项目的情况进行配置,点击加号➕,产生空槽,将场景中需要标定的相机拖入,一个或者多个均可,同时,此字段是公开的,可以利用脚本进行赋值:

    点击➕,拖入待标定相机

    利用脚本进行赋值

  3. 调整相机分辨率,点击Game,选择一个具体的分辨率,此处以1920*1080为例

    选择一个具体的分辨率

  4. 传入一个Transform,chessboardGenerateCenter,用来指示虚拟棋盘的产生位置,同时也规定了标定参照的水平面,这个 Transform 的位置最好能在所需标定的相机的屏幕中央附近(此处为了演示此位置,创建了一个cube,实际使用中只需要创建一个空物体,传入Transform即可,不必考虑其旋转,将被统一清零):

    位置最好能在所需标定的相机的屏幕中央附近

    传入一个Transform

  5. 给定目标文件夹,CalibrateTool会在此文件夹下产生一个 calib 文件夹用来保存数据。一般会填入MultiviewX所在文件夹

    给定目标文件夹

  6. 传入一个Transform,Grid Origin用来指示Grid格点的原点,同时也是OpenCV坐标系(右手坐标系)的原点,为了方便计算,应当将此点设置在标定参照的水平面上(其Unity坐标的Y值应该与chessboardGenerateCenter的Y值相同,我没有测试过不相同会如何)。

    Grid Origin

    正确配置后,Scene场景中会产生辅助线。此图中,蓝色箭头指示右手坐标系下Y轴的正反向,红色箭头指示右手坐标系下X轴的正方向

    指示Grid格点的原点

  7. 可以通过调节MAP_HEIGHTMAP_WIDTH 来调节格点图的大小,MultivewX只会标注脚底在格点图中的人。默认值16 与 25 是一个合理的值,一般不需要额外改动。

    例如,当MAP_HEIGHT = 16 ,MAP_WIDTH = 8 时,标注如图:

    MAP\_WIDTH = 8

    bbox\_cam7

    MAP_EXPAND可以理解成每个边长被额外划分多少份(小刻度),改动此项不会改变地图的大小。

    例如,当MAP_EXPAND = 40 时:

    MAP\_EXPAND = 40

  8. 缩放(Scaling)。很多时候,场景素材的地图尺寸和人物模型尺寸是不一样的,往往会存在人物模型与场景的不协调。 下图展示了这种差异,人物模型看起来很小,成年人看起来身高和儿童一样。

    地图尺寸和人物模型尺寸这就导致用户难以一站式完成CalibrateTool的使用,需要自己手动再去调整人物模型或者场景素材,而往往这种调整还牵扯到matchings(MultiviewX接受的一种输入)的坐标变换,每个人实现matchings的方法都不一样,这里提个醒,坐标变换的顺序必须是: 缩放->旋转->平移

    通过调整Scaling参数,CalibrateTool的辅助线与辅助模型(立方体)可以帮助用户很直观的找到一个合理的缩放值。辅助线的每格的边长为右手坐标系下1米。辅助模型的长宽为MAN_RADIUS*2,高为MAN_HEIGHT

    给与生成辅助线所需引用

    辅助线与模型

  9. 一般情况下,其余参数不需要额外设置。如果Python端报错,可以尝试调大Update Chessboard Interval参数,增加IO读写的时间。

  10. 运行。

Calibrate

拿到数据后,我们就可以进行标定了,应该会有如下结构:calib文件夹下有 C1 - Cn 子文件夹,每个子文件夹中,有得到的棋盘数据。

image-20230301221916504

运行calibrateCameraByChessboard.py,内外参数分别保存在calibration/intrinsiccalibration/extrinsic中。所输出的外参数(对于每一个虚拟的棋盘,都有一个外参负责对应的变换)为对第一个虚拟棋盘的变换,且生成的第一个虚拟棋盘总是与给定的Chessboard Generate Center同面

虚拟棋盘始终与给定的Chessboard Generate Center同面

一个疑惑 这种方法中,Python收到的棋盘格的世界坐标是给定的,如上图所示。(0,0,0)总是在左下角 往往在一些相对Chessboard Generate Center物体对称的物体,会有一个很相近的tvec。 因为在单个相机时,相对于该相机描述时,物体总是需要做同样的位移变换。但是在多个相机时,这种描述,依旧是相对单个接受标定的相机。

为了解决上述的疑问,尝试采用了cv2.solvePnP,和一组在待标定(水)平面上的静态的点来得到R与T,目前不支持相对斜面标定外参。

一组在待标定平面上的静态的点

在引入Grid格点的原点的概念时,遇到了一些问题。原思路是直接变换MarkPoints,使得solvePnP ”认识“目标坐标系,但是,经过实验得知,变换MarkPoints时候,应当保证最终被solvePnP获取的(OpenCV中)世界坐标的x,y值,保证其正方向与原 Unity 中x,z正方向保持一致或全部相反。否则会导致后续POM生成失败。猜测是左右手坐标系变换后OpenCV下Y正方向的问题。

Validate

通过参考Unity3d和OpenCV的相机模型左右手坐标系下三维位姿(旋转、平移)的转换旋转向量和旋转矩阵的互相转换 python cv2.Rodrigues()得知,默认的 Unity3D 相机组件是一个理想的针孔相机,其内外参可以通过调用Unity3D给予的相关参数计算得出。

GetNativeCalibrationByMath(),给出了这种方法,其结果基于Unity场景下的世界坐标系。经过比较这两个方法获得的值,误差很小,可以认为CalibrateTool是可以合理利用的。

提供了 vali.py,其利用单应性检验标定结果。

Preview

cam1\_frames

支持动图预览

  • 例如,在命令行中输入 python run_all.py -p 15 ,即可为前15帧生成带标注框的动图预览。

Work with MultiviewX_Perception

请 clone 一份 MultiviewX_Perception,可以参考Notes of MultiviewX_Perception进行后续工作。其将原calibdateCamera.py替换为了calibrateByChessboard.py,添加了一些动态容量的数组以适应不同的摄像头数,支持Scaling缩放,并且datasetParameters将由 CalibrateTool 根据Unity中Inspector面板处的参数自动生成,等等。

欢迎下载示例文件sample.zip,将其子文件夹calibperceptionmatchings,子文件datasetParameters.py拖入到 MultiviewX_Perception 文件夹下。

黄色字体即拖入的文件

运行run_all.py,参考Notes of MultiviewX_Perception,其给出了一些常用的命令行参数

1
python run_all.py 

当用户仅仅需要标定与生成POM时候(仅仅提供calibdatasetParameters.py),可以输入参数-f:

1
python run_all.py -f 

如果遇到WinError 32 报错,请检查是否有相关程序正在使用.pom,在PyCharm中,可能不小心打开了.pom的预览窗口,请关闭。

image-20230325105913670

示例 datasetParameters.py (for Wildtrack):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
GRID_ORIGIN = [-14.91,-1.51,-5.43]
NUM_CAM = 7
CHESSBOARD_COUNT = 50
MAP_WIDTH = 12
MAP_HEIGHT = 36
MAP_EXPAND = 40
IMAGE_WIDTH = 1280
IMAGE_HEIGHT = 720
MAN_HEIGHT = 1.8
MAN_RADIUS = 0.16
RJUST_WIDTH = 4
Scaling = 1
NUM_FRAMES = 0
DATASET_NAME = ''

# If you are using perception package: this should NOT be 'perception', output path of perception instead
PERCEPTION_PATH = 'D:/Test/WildPerception'

# The following is for -view configure only:

# Define how to convert your unit length to meter, if you are using cm, then 0.01
OverlapUnitConvert = 0.01
# Define how to translate cams to make the world origin and the grid origin is the same
OverlapGridOffset = (3., 9., 0.)

使用Pandas和matplotlib库进行简单的数据分析与可视化

Introduction

Pandas 是 Python 语言的一个扩展程序库,用于数据分析。

Matplotlib is a comprehensive library for creating static, animated, and interactive visualizations in Python. Matplotlib makes easy things easy and hard things possible.

本文章将会对一些数据data.csv进行处理与绘图,形如:

Idx Title of Book Description Authors Rating Price Availability Book Category
0 It’s Only the Himalayas Wherever you go, whatever you do, just . . . don’t do anything stupid. S. Bedford 2 45.17 19 Travel

Analysis

Read File

利用pd.read_csv完成读取,指定index_col字段,以指定数据随后所使用的索引。

1
2
3
4
5
import matplotlib.pyplot as plt
import pandas as pd
pd.set_option('display.width', 500)
pd.set_option('display.max_columns', 100)
dfcand=pd.read_csv("data.csv", sep=',',index_col=0)

Group by

dataframe对象使用.groupby()可以对数据进行合理的归并分组,是一个pandas.core.groupby.generic.DataFrameGroupBy 对象

.size()

.size()字段可以得出各个分组的名称和对应大小(数量),是一个pandas.core.series.Series对象

1
2
3
4
5
6
7
8
9
10
11
12
dfcand.groupby('Book Category').size()

#Book Category
#Academic 1
#Add a comment 67
#Adult Fiction 1
#Art 8
#Autobiography 9
#Biography 5
#...
#Young Adult 54
#dtype: int64

Series对象

.sort_values(ascending=False)

Series对象,.sout_values()可以规定其排序方式

1
2
3
4
5
6
7
8
9
10
11
12
piedata = dfcand.groupby('Book Category').size().sort_values(ascending=False)

#Book Category
#Default 152
#Nonfiction 110
#Sequential Art 75
#Add a comment 67
#Fiction 65
#Young Adult 54
#...
#Academic 1
#dtype: int64

一般的,对于Series对象,可以利用比较符号进行筛选,例如,我们要获得以上大于7的值

1
2
3
4
5
6
7
8
abovepiedata = piedata[piedata>7]

#Book Category
#Default 152
#Nonfiction 110
#...
#Autobiography 9
#dtype: int64

pd.concat()

可以使用此方法将多个Series对象合成为dataframe对象

当axis = 1时,如果其索引一样,会将合并的Series作为新的列,最终合并为dataframe

1
pd3d = pd.concat([ratingseries,availabilityseries,sizeseries],axis= 1,ignore_index= False)

对于合并后可能出现的未命名列,可以使用.iloc 获取,例如

1
pd3d.iloc[:,2]

Benford

By Benford’s law it is often the case that 1 occurs more frequently than 2, 2 more frequently than 3, and so on. This observation is a simplified version of Benford’s law. More precisely, the law gives a prediction of the frequency of leading digits using base-10 logarithms that predicts specific frequencies which decrease as the digits increase from 1 to 9.

1
2
3
4
frequency = {'1':0,'2':0,'3':0,'4':0,'5':0,'6':0,'7':0,'8':0,'9':0}
def benford(num):
firstdigit = str(num)[0]
frequency[firstdigit] = frequency[firstdigit] + 1

Draw diagrams

General

1
2
3
4
5
6
7
8
9
plt.figure(figure = (10,10) #规定画布的大小
plt.rcParams.update({'font.family': 'Times New Roman'}) #规定显示字体
plt.rcParams.update({'font.weight': 'normal'}) #规定字体粗细
plt.rcParams.update({'font.size': 15}) #规定字体字号

#Draw Diagram

plt.title("This is a figure") #规定图片标题
plt.show() #显示图片

Pie

Series对象,可利用.values获取其值

利用plt.pie()绘制饼图

第一个参数提供数据

labels = 提供对应数据的标签

startangle = 规定第一个刻度的角度27a6d1f0aa648134a54357c580480631.png

labeldistance = 规定标签到饼图的距离

1
plt.pie(abovepiedata.values,labels=abovepiedata.index,startangle= 32,labeldistance= 1.12)

3D Scatter

1
2
3
4
5
6
7
ax = plt.axes(projection = '3d') #规定为3D散点图
ax.scatter3D(pd3d['Rating'], pd3d.iloc[:,2], pd3d['Availability']) #为散点图提供数据
plt.xticks((range(5)) #规定x轴的刻度标度

plt.xlabel('Avg Rating')#规定x轴标签
plt.ylabel('Size of Category',rotation = 39)#规定y轴标签
ax.set_zlabel('Avg stock')#规定z轴标签

60be8e80839f1377b500f8880e84589a.png

Bar & Plot

plt.bar() 中,x = 提供了数据的条目(有几列数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
plt.bar(x=range(len(benser)), height=benser, label='Data', tick_label = benser.index)

plt.xlabel("First Digit")
plt.ylabel("Data")

#与上图共用同一个x轴
ax2 = plt.twinx()

#规定y轴的取值区间
ax2.set_ylim([0.04, 0.33]);
plt.plot(range(len(benser)), y, marker='.', color ='goldenrod', linewidth='1', label="Benford's Law")

#给出折线上的百分比图例
plt.legend(loc="upper right")
for a, b in zip(range(len(benser)), y):
plt.text(a, b, str('{:.2f}'.format(b*100)) + '%', ha='center', va='bottom', fontsize=8)

31c32aecc6b84bc4f1c38402c0cfb495.png

How to get the author of the book by title

Introduction

思路很简单,找到一个 API 即可。

此 API 接受书名(Title)作为参数,返回的数据中须包含作者(Author)字段。

经过一些探索,Google Books APIs 可以作为一个合格的解决办法。而Goodreads.com的搜索框可以是一个辅助选项。

Google Books APIs

Refer to this webpage to find more information: Google Books APIs Getting Started

Google Books has a vision to digitize the world’s books. You can use the Google Books API to search content, organize an authenticated user’s personal library and modify it as well.

Books concepts

为了能够正确处理随后返回的json数据,应当理解以下四则基本概念:

  • Volume: A volume represents the data that Google Books hosts about a book or magazine. It is the primary resource in the Books API. All other resources in this API either contain or annotate a volume.

  • Bookshelf: A bookshelf is a collection of volumes. Google Books provides a set of predefined bookshelves for each user, some of which are completely managed by the user, some of which are automatically filled in based on user’s activity, and some of which are mixed. Users can create, modify or delete other bookshelves, which are always filled with volumes manually. Bookshelves can be made private or public by the user.

    Note: Creating and deleting bookshelves as well as modifying privacy settings on bookshelves can currently only be done through the Google Books site.

  • Review: A review of a volume is a combination of a star rating and/or text. A user can submit one review per volume. Reviews are also available from outside sources and are attributed appropriately.

  • Reading Position: A reading position indicates the last read position in a volume for a user. A user can only have one reading position per volume. If the user has not opened that volume before, then the reading position does not exist. The reading position can store detailed position information down to the resolution of a word. This information is always private to the user.

Working with volumes

You can perform a volumes search by sending an HTTP GET request to the following URI:

https://www.googleapis.com/books/v1/volumes?q=search+terms

例如:搜索书目《It’s Only the Himalayas》,如果配置正确,会得到一个json,内含所有的搜索结果,一般的,认为第一个结果就是我们搜索得到的书目。

Goodreads.com

Refer to this webpage to find more information: Goodreads.com

Discover and share books you love on Goodreads, the world’s largest site for readers and book recommendations!

实例请求:搜索《Test》

对于搜索请求https://www.goodreads.com/search?q=`keyword`&qid=

只需要将关键词填入q=后

Scrape

Goodreads.com 可能有反爬机制,可以使用伪装浏览器的办法缓解一些情况:

1
2
3
4
5
6
7
8
send_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Connection": "keep-alive",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.8"
}

result = requests.get(main_url, send_headers, proxies=proxies)

一些情况下Goodreads也无法给出正确的作者,需要使用try catch关键字捕获异常情况。

Python Code

注意事项

对于在Python中使用这两个办法,需要注意一些内容

  1. 代理可能导致错误request eof occurred in violation of protocol (_ssl.c:997)

    多见于使用的代理工具代理模式为全局代理,并且未在Python脚本中正确配置代理,尝试通过以下办法解决:

    1
    2
    3
    4
    5
    6
    7
    8
    import requests
    proxies = {
    'http': 'http://your_server:your_port',
    'https': 'http://your_server:your_port',
    }

    #仅在需要代理的请求下,填写参数proxies=proxies
    result = requests.get(Full_API_Link,proxies = proxies)
  2. Google Books APIs 所返回的json中,结果title字段下的标题并不与本地title字段相匹配。通过使用Python自带的 difflib 库实现匹配功能。

  3. 由于持续的请求,可能会导致请求失败,故使用try: except:关键字捕获异常,保证程序正常运行。

简单实现

假设:

  1. 本电脑使用的代理工具是CFW,未开启全局代理,默认端口

  2. 可以正常请求到对应书目,遍历json找到最为匹配的标题。

  3. Google Books APIs 无法正确获得作者,进而使用Goodreads尝试获取之。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import requests
proxies = {
'http': 'http://localhost:7890',
'https': 'http://localhost:7890',
}
import json
import difflib、
from bs4 import BeautifulSoup

def UrlToSoupAdvanced(Url:str):
main_url = Url
print(main_url)
send_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Connection": "keep-alive",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.8"
}

try:
result = requests.get(main_url, send_headers, proxies=proxies)
except :
time.sleep(5)
return "[Unknown][Goodreads]Fail to request the link"

return BeautifulSoup(result.text, 'html.parser')

def TwoStrMatch(string1:str,string2:str):
result = difflib.SequenceMatcher(None, string1, string2).quick_ratio()
print(string1 + "====" + string2 + "====" + str(result))
return result

def GetAuthorByTitleUsingGoogleBooksAPI(Title:str):
Google_Book_APIs_Head = "https://www.googleapis.com/books/v1/volumes?q="
API_KEY = "AIzaSyCdlpdS8EWgKIN6EW95fwjiLqDkkiIA8Pg"
Search_Data = Title

#https://www.googleapis.com/books/v1/volumes?q=A%20Summer%20In%20Europe+intitle:keyes&key=AIzaSyCdlpdS8EWgKIN6EW95fwjiLqDkkiIA8Pg
#https://www.googleapis.com/books/v1/volumes?q=a summer in europe&printType=books&intitle:a summer in europe

Full_API_Link = Google_Book_APIs_Head + Search_Data
#Full_API_Link = Google_Book_APIs_Head + "It's_Only_the_Himalayas"

print("The request link is " + Full_API_Link)
try:
result = requests.get(Full_API_Link,proxies = proxies)
except :
time.sleep(5)
return GetAuthorByTitleUsingGoodreadsSearch(Title)



return_json_dict = json.loads(s = result.text)
if 'items' in return_json_dict:
return_items = return_json_dict['items']
else:
return GetAuthorByTitleUsingGoodreadsSearch(Title)

#对于返回的搜索结果
for book in return_items:
current_volumn = book['volumeInfo']
current_title = current_volumn['title']

if 'authors' in current_volumn:
current_authors = current_volumn['authors']

#如果标题直接匹配,正常返回
if (TwoStrMatch(current_title, Title) > 0.85):
return str(','.join(current_authors))

if('subtitle' in current_volumn):
#如果加上副标题是匹配的,则正常返回
if (TwoStrMatch(current_title + current_volumn['subtitle'], Title) > 0.85):
return str(','.join(current_authors))

return GetAuthorByTitleUsingGoodreadsSearch(Title)

def GetAuthorByTitleUsingGoodreadsSearch(Title:str):

Full_Search_Link = "https://www.goodreads.com/search?q=" + title +"&qid="


try:
soup = UrlToSoupAdvanced(Full_Search_Link)
except:
return "[Unknown][Goodreads]Fail to request the link"

try:
author = soup.find('span', itemprop="author").div.a.span.string
except:
return "[Unknown][Goodreads]No correct Author"

if(author == None):
return "[Unknown][Goodreads]No correct Author"
return author