처음에는 메인페이지를 맡기로 했었는데 프론트에서 끝내주셔서 약 등록 페이지를 맡게 됐다.
원래 필요한 데이터셋을 다운받아서 opencv 에 학습시키겠다는 야심찬 계획을 세웠었지만 우선 데이터의 크기가 테라바이트 단위였고, 그마저도 약은 워낙 방대하고 비슷하게 생긴 경우가 많아 제대로 인식이 되는 경우가 드물었다. 사실 여기서 약이라는 주제를 생각하지 말았어야 했나 하고 후회했다... 게다가 약의 정보를 따로 데이터베이스에 저장해야 할 필요성까지... 그럴 수 있을 정도의 시간과 돈이 없었다.
그래서 결국 데이터셋 몇 개만 갖고 특정 이미지만 학습시킨 후 selenium을 통해 검색을 한 뒤, beautifulSoup 으로 크롤링을 해 저장하는 방식으로 타협을 보았다.
이를 위한 첫번째 단계로, Roboflow 에서 데이터셋을 찾았다. 원래는 ResNet 이나 opencv 를 쓰고 싶었지만 약이란 게 텍스트만 써져있는 것도 아니고 텍스트만 써져있는 경우도 그림자나 굴곡으로 인해 텍스트와 색깔 추출이 제대로 되지 않았다. 해서 조금 과하긴 해도 조금 쉽게 할 수 있을 것 같은 yolo(you only look once) 모델을 이용하기로 했다.
데이터셋과 모델은 여기에서 가져왔다.
Roboflow 는 pre-trained 된 모델이 있는 경우 overview 하단의 api 를 통해 이를 쓸 수 있다. 직접 학습도 시켜봤지만 내 노트북이 우는 것 같은 기분이 들어 중간에 멈추고 그냥 api 를 썼다.
Drug/views.py
class DrugCreateView(APIView):
def post(self, request):
print(request.data["img"])
uploaded_img = request.data["img"]
if not uploaded_img:
return Response({"error": "이미지가 없습니다."}, status=status.HTTP_400_BAD_REQUEST)
img_data = uploaded_img.read()
image = Image.open(BytesIO(img_data))
image = np.array(image)
cv2.imwrite("./media/UploadDrug/pill.jpg", image)
img = "./media/UploadDrug/pill.jpg"
print(img)
# 이미지 파일 목록을 취득(패스)
pics = glob.glob(img)
# 조정 후 사이즈를 지정(베이스 이미지)
size = (960, 960)
# 리사이즈 처리
for pic in pics:
base_pic = np.zeros((size[1], size[0], 3), np.uint8)
pic1 = cv2.imread(pic, cv2.IMREAD_COLOR)
h, w = pic1.shape[:2]
ash = size[1] / h
asw = size[0] / w
# 크기 비율 맞추기
if asw < ash:
sizeas = (int(w * asw), int(h * asw))
else:
sizeas = (int(w * ash), int(h * ash))
# 비율 맞춰 줄인 사진
pic1 = cv2.resize(pic1, dsize=sizeas)
base_pic[
int(size[1] / 2 - sizeas[1] / 2) : int(size[1] / 2 + sizeas[1] / 2),
int(size[0] / 2 - sizeas[0] / 2) : int(size[0] / 2 + sizeas[0] / 2),
:,
] = pic1
# print(base_pic.shape)
cv2.imwrite("./media/" + pic, base_pic)
# infer on a local image
response = model.predict(pic, confidence=40, overlap=30).json()
# print(response)
# 검색 결과가 존재할 시 응답을 selenium 으로 검색하여 정보 저장
if response["predictions"]:
drug_name = response["predictions"][0]["class"]
print(drug_name)
driver = webdriver.Chrome()
url = "https://pharmeasy.in/"
driver.get(url)
try:
driver.find_element(
By.XPATH,
'//*[@id="__next"]/main/div[3]/div[1]/div/div[1]/div/div[2]/div/div[1]',
).click()
driver.find_element(By.XPATH, '//*[@id="topBarInput"]').send_keys(
f"{drug_name}"
)
driver.find_element(By.XPATH, '//*[@id="topBarInput"]').send_keys(
Keys.RETURN
)
driver.find_element(
By.CSS_SELECTOR,
"#__next > main > div > div > div > div.LHS_container__mrQkM.Search_fullWidthLHS__mteti > div:nth-child(2) > div > div > a",
).click()
url = driver.current_url
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36"
}
data = requests.get(url, headers=headers)
soup = BeautifulSoup(data.text, "html.parser")
drug_img = soup.find("img", attrs={"alt": "img"}).get("src")
print(drug_img)
drug_form = (
soup.select_one(
"#__next > main > div > div.Content_container__oOxF6 > div.LHS_container__mrQkM > div.PDPDesktop_infoContainer__LCH8b > div.MedicineOverviewSection_medicineUnitContainer__F6ZV_ > div > div > div > div.MedicineOverviewSection_measurementUnit__7m5C3"
)
.get_text()
.strip()
)
drug_company = (
soup.select_one(
"#__next > main > div > div.Content_container__oOxF6 > div.LHS_container__mrQkM > div.PDPDesktop_infoContainer__LCH8b > div.MedicineOverviewSection_medicineUnitContainer__F6ZV_ > div > div > div > div.MedicineOverviewSection_brandName__rJFzE"
)
.get_text()
.strip()
)
drug_ingre = soup.select_one(
"#__next > main > div > div.Content_container__oOxF6 > div.LHS_container__mrQkM > div.DescriptionTable_descriptionTableContainer__YLlE4 > div.DescriptionTable_descriptionTable__TRPw2 > table > tbody > tr:nth-child(3) > td.DescriptionTable_value__0GUMC"
).get_text()
# 효능 / 효과
drug_eff = soup.select_one(
"#__next > main > div > div.Content_container__oOxF6 > div.LHS_container__mrQkM > div.DescriptionTable_descriptionTableContainer__YLlE4 > div.DescriptionTable_descriptionTable__TRPw2 > table > tbody > tr:nth-child(4) > td.DescriptionTable_value__0GUMC"
).get_text()
drug_data = {
"name": drug_name,
"company": drug_company,
"drug_image": drug_img,
"form": drug_form,
"ingredient": drug_ingre,
}
print(drug_data)
try:
Drug.objects.get(name=drug_name)
return Response(
{"message": "이미 저장된 알약입니다."}, status=status.HTTP_200_OK
)
except:
Drug.objects.create(
name=drug_data["name"],
company=drug_data["company"],
drug_image=drug_data["drug_image"],
form=drug_data["form"],
ingredient=drug_data["ingredient"],
)
return Response(
{"message": "등록 완료"}, status=status.HTTP_201_CREATED
)
except:
return Response(
{"error": "등록되어 있지 않은 알약입니다. 정보를 직접 입력해주세요"},
status=status.HTTP_400_BAD_REQUEST,
)
else:
return Response(
{"error": "인식할 수 없습니다. 정보를 직접 입력해주세요"},
status=status.HTTP_400_BAD_REQUEST,
)
uploaded_img = request.data["img"]
if not uploaded_img:
return Response({"error": "이미지가 없습니다."}, status=status.HTTP_400_BAD_REQUEST)
img_data = uploaded_img.read()
image = Image.open(BytesIO(img_data))
image = np.array(image)
cv2.imwrite("./media/UploadDrug/pill.jpg", image)
img = "./media/UploadDrug/pill.jpg"
print(img)
# 이미지 파일
pics = glob.glob(img)
# 조정 후 사이즈 지정
size = (960, 960)
# resize
for pic in pics:
base_pic = np.zeros((size[1], size[0], 3), np.uint8)
pic1 = cv2.imread(pic, cv2.IMREAD_COLOR)
pic1 = cv2.cvtColor(pic1, cv2.COLOR_BGR2RGB)
h, w = pic1.shape[:2]
ash = size[1] / h
asw = size[0] / w
# 크기 비율 맞추기
if asw < ash:
sizeas = (int(w * asw), int(h * asw))
else:
sizeas = (int(w * ash), int(h * ash))
# 비율 맞춰 줄인 사진
pic1 = cv2.resize(pic1, dsize=sizeas)
base_pic[
int(size[1] / 2 - sizeas[1] / 2) : int(size[1] / 2 + sizeas[1] / 2),
int(size[0] / 2 - sizeas[0] / 2) : int(size[0] / 2 + sizeas[0] / 2),
:,
] = pic1
# print(base_pic.shape)
cv2.imwrite("./media/" + pic, base_pic)
모델을 학습하는 데에 쓴 사진들이 다 960*960 이길래 따로 resize 가 필요한가 싶어 비율대로 늘이거나 줄여 남은 부분은 검은 여백(영행렬)을 주는 식으로 처리했지만 다시 보니 딱히 필요없을 것 같다. 그래도 나중에 쓸 일이 있을 것 같아 일단 기록해둔다.
이미지 전처리시 색깔 변환을 해주지 않아 빨간색 알약이 파란색으로 변해 전송되는 오류가 있었다. 여기를 참고하여 수정했다.
response = model.predict(pic, confidence=40, overlap=30).json()
# print(response)
# 검색 결과가 존재할 시 응답을 selenium 으로 검색하여 정보 저장
if response["predictions"]:
drug_name = response["predictions"][0]["class"]
print(drug_name)
driver = webdriver.Chrome()
url = "https://pharmeasy.in/"
driver.get(url)
try:
driver.find_element(
By.XPATH,
'//*[@id="__next"]/main/div[3]/div[1]/div/div[1]/div/div[2]/div/div[1]',
).click()
driver.find_element(By.XPATH, '//*[@id="topBarInput"]').send_keys(
f"{drug_name}"
)
driver.find_element(By.XPATH, '//*[@id="topBarInput"]').send_keys(
Keys.RETURN
)
driver.find_element(
By.CSS_SELECTOR,
"#__next > main > div > div > div > div.LHS_container__mrQkM.Search_fullWidthLHS__mteti > div:nth-child(2) > div > div > a",
).click()
model로 추론한 결과를 response 에 저장한다. 해당 response 의 class 에는 추론 결과 - 약 이름 - 이 담겨 있다. 이 약 이름을 selenium 을 통해 사이트에 검색하고, 제일 첫 번째로 뜨는 결과를 클릭하여 들어간다.
검색창을 클릭하여 send_keys('약이름') 을 할 시 검색 버튼이 사라지므로 다시 검색창 요소를 받아와 send_keys(Keys.RETURN)
즉 엔터키를 눌러 페이지가 넘어가게 처리했다.
url = driver.current_url
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36"
}
data = requests.get(url, headers=headers)
soup = BeautifulSoup(data.text, "html.parser")
drug_img = soup.find("img", attrs={"alt": "img"}).get("src")
print(drug_img)
drug_form = (
soup.select_one(
"#__next > main > div > div.Content_container__oOxF6 > div.LHS_container__mrQkM > div.PDPDesktop_infoContainer__LCH8b > div.MedicineOverviewSection_medicineUnitContainer__F6ZV_ > div > div > div > div.MedicineOverviewSection_measurementUnit__7m5C3"
)
.get_text()
.strip()
)
drug_company = (
soup.select_one(
"#__next > main > div > div.Content_container__oOxF6 > div.LHS_container__mrQkM > div.PDPDesktop_infoContainer__LCH8b > div.MedicineOverviewSection_medicineUnitContainer__F6ZV_ > div > div > div > div.MedicineOverviewSection_brandName__rJFzE"
)
.get_text()
.strip()
)
drug_ingre = soup.select_one(
"#__next > main > div > div.Content_container__oOxF6 > div.LHS_container__mrQkM > div.DescriptionTable_descriptionTableContainer__YLlE4 > div.DescriptionTable_descriptionTable__TRPw2 > table > tbody > tr:nth-child(3) > td.DescriptionTable_value__0GUMC"
).get_text()
# 효능 / 효과
drug_eff = soup.select_one(
"#__next > main > div > div.Content_container__oOxF6 > div.LHS_container__mrQkM > div.DescriptionTable_descriptionTableContainer__YLlE4 > div.DescriptionTable_descriptionTable__TRPw2 > table > tbody > tr:nth-child(4) > td.DescriptionTable_value__0GUMC"
).get_text()
drug_data = {
"name": drug_name,
"company": drug_company,
"drug_image": drug_img,
"form": drug_form,
"ingredient": drug_ingre,
}
driver.current_url
로 현재 url 을 받아온 다음 BeautifulSoup 을 통해 필요한 정보를 크롤링하여 drug_data 에 딕셔너리 형태로 저장해주었다. 이때 필요없는 공백과 \n 을 없애기 위해 .strip()
을 사용했다. 이미지는 주소로 저장하여 별도의 미디어 파일 없이도 프론트에서 뜰 수 있도록 할 것이다.
try:
Drug.objects.get(name=drug_name)
return Response(
{"message": "이미 저장된 알약입니다."}, status=status.HTTP_200_OK
)
except:
Drug.objects.create(
name=drug_data["name"],
company=drug_data["company"],
drug_image=drug_data["drug_image"],
form=drug_data["form"],
ingredient=drug_data["ingredient"],
)
먼저 try 로 해당 약 이름이 있는지 검사한 다음 있으면 이미 있는 알약이라는 메세지를, 아니면 새로 create 해준다.
새로운 방향으로 프로젝트를 진행하는데 큰 결저이 필요했을텐데 타협점을 찾고 구현을 해내는 과정이 좋습니다👍