├── bookshelf_reader ├── __init__.py ├── views.py ├── wsgi.py ├── urls.py └── settings.py ├── detect_spines ├── __init__.py ├── api │ ├── __init__.py │ ├── urls.py │ ├── serializers.py │ └── views.py ├── tests.py ├── views.py ├── apps.py ├── admin.py └── models.py ├── assets ├── spines.jpg └── drawn_spines.jpeg ├── requirements.txt ├── manage.py ├── .gitignore ├── LICENSE ├── scrap_book.py ├── README.md └── spine_detection.py /bookshelf_reader/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bookshelf_reader/views.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /detect_spines/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /detect_spines/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /detect_spines/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /detect_spines/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /assets/spines.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LakshyaKhatri/Bookshelf-Reader-API/HEAD/assets/spines.jpg -------------------------------------------------------------------------------- /assets/drawn_spines.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LakshyaKhatri/Bookshelf-Reader-API/HEAD/assets/drawn_spines.jpeg -------------------------------------------------------------------------------- /detect_spines/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DetectSpinesConfig(AppConfig): 5 | name = 'detect_spines' 6 | -------------------------------------------------------------------------------- /detect_spines/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Bookshelf, Spine, Book 3 | 4 | # Register your models here. 5 | admin.site.register(Bookshelf) 6 | admin.site.register(Spine) 7 | admin.site.register(Book) 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -f /usr/share/pip-wheels 2 | asgiref>=3.2.3 3 | beautifulsoup4>=4.8.2 4 | certifi>=2019.11.28 5 | chardet>=3.0.4 6 | Django>=3.0.3 7 | djangorestframework>=3.11.0 8 | google>=2.0.3 9 | idna>=2.8 10 | numpy>=1.18.1 11 | opencv-python>=4.2.0.32 12 | Pillow>=7.0.0 13 | pytz>=2019.3 14 | requests>=2.22.0 15 | soupsieve>=1.9.5 16 | sqlparse>=0.3.0 17 | urllib3>=1.25.8 18 | -------------------------------------------------------------------------------- /bookshelf_reader/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for bookshelf_reader project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bookshelf_reader.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /detect_spines/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, re_path 2 | 3 | from .views import ( 4 | CreateBookshelfView, 5 | GetBookshelfView, 6 | SpineListView, 7 | AddBookView, 8 | GetBookView 9 | ) 10 | 11 | 12 | urlpatterns = [ 13 | path('create-bookshelf/', CreateBookshelfView.as_view()), 14 | path('bookshelf//', GetBookshelfView.as_view()), 15 | re_path(r'spines/(?P\d+)/', SpineListView.as_view()), 16 | path('add-book/', AddBookView.as_view()), 17 | path('books//', GetBookView.as_view()) 18 | ] 19 | -------------------------------------------------------------------------------- /detect_spines/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from detect_spines.models import Bookshelf, Spine, Book 3 | 4 | 5 | class BookshelfSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = Bookshelf 8 | fields = "__all__" 9 | 10 | 11 | class SpineListSerializer(serializers.ModelSerializer): 12 | class Meta: 13 | model = Spine 14 | fields = ("image", ) 15 | 16 | 17 | class BookSerializer(serializers.ModelSerializer): 18 | class Meta: 19 | model = Book 20 | fields = "__all__" 21 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bookshelf_reader.settings') 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Environments 60 | .env 61 | .venv 62 | env/ 63 | venv/ 64 | ENV/ 65 | env.bak/ 66 | venv.bak/ 67 | migrations/ 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lakshya Khatri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bookshelf_reader/urls.py: -------------------------------------------------------------------------------- 1 | """bookshelf_reader URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | 17 | from django.conf import settings 18 | from django.conf.urls.static import static 19 | 20 | from django.contrib import admin 21 | from django.urls import path, include 22 | 23 | urlpatterns = [ 24 | path('admin/', admin.site.urls), 25 | 26 | # rest framework: 27 | path('api-auth/', include('rest_framework.urls')), 28 | path("api/", include('detect_spines.api.urls')) 29 | ] 30 | 31 | if settings.DEBUG: 32 | urlpatterns = urlpatterns + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 33 | urlpatterns = urlpatterns + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 34 | -------------------------------------------------------------------------------- /detect_spines/api/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from detect_spines.models import ( 3 | Bookshelf, 4 | Spine, 5 | Book 6 | ) 7 | 8 | from .serializers import ( 9 | BookshelfSerializer, 10 | SpineListSerializer, 11 | BookSerializer 12 | ) 13 | from rest_framework import status 14 | from rest_framework.response import Response 15 | 16 | 17 | class CreateBookshelfView(generics.CreateAPIView): 18 | queryset = Bookshelf.objects.all() 19 | serializer_class = BookshelfSerializer 20 | 21 | def create(self, request, *args, **kwargs): 22 | serializer = self.get_serializer(data=request.data) 23 | serializer.is_valid(raise_exception=True) 24 | obj = serializer.save() 25 | self.perform_create(serializer) 26 | headers = self.get_success_headers(serializer.data) 27 | headers['id'] = obj.id 28 | response = Response({"Success": "Created Successfully"}, 29 | status=status.HTTP_201_CREATED, headers=headers) 30 | return response 31 | 32 | 33 | class GetBookshelfView(generics.RetrieveAPIView): 34 | queryset = Bookshelf.objects.all() 35 | serializer_class = BookshelfSerializer 36 | 37 | 38 | class SpineListView(generics.ListAPIView): 39 | serializer_class = SpineListSerializer 40 | 41 | def get_queryset(self): 42 | bookshelf_pk = self.kwargs['bookshelf_pk'] 43 | bookshelf = Bookshelf.objects.filter(id=bookshelf_pk)[0] 44 | queryset = Spine.objects.filter(bookshelf=bookshelf) 45 | return queryset 46 | 47 | 48 | class AddBookView(generics.CreateAPIView): 49 | queryset = Book.objects.all() 50 | serializer_class = BookSerializer 51 | 52 | def create(self, request, *args, **kwargs): 53 | serializer = self.get_serializer(data=request.data) 54 | serializer.is_valid(raise_exception=True) 55 | obj = serializer.save() 56 | self.perform_create(serializer) 57 | headers = self.get_success_headers(serializer.data) 58 | headers['id'] = obj.id 59 | response = Response({"Success": "Created Successfully"}, 60 | status=status.HTTP_201_CREATED, headers=headers) 61 | return response 62 | 63 | 64 | class GetBookView(generics.RetrieveAPIView): 65 | queryset = Book.objects.all() 66 | serializer_class = BookSerializer 67 | -------------------------------------------------------------------------------- /scrap_book.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | import requests 3 | import re 4 | from googlesearch import search 5 | 6 | 7 | class BookInfo: 8 | 9 | def __init__(self, **kwargs): 10 | self.title = kwargs.get("title") 11 | self.author = kwargs.get("author") 12 | self.image_url = kwargs.get("image_url") 13 | self.publisher = kwargs.get("publisher") 14 | self.isbn_10 = kwargs.get("isbn_10") 15 | self.isbn_13 = kwargs.get("isbn_13") 16 | self.rating = kwargs.get("rating") 17 | self.description = kwargs.get("description") 18 | self.total_pages = kwargs.get("total_pages") 19 | self.genre = kwargs.get("genre") 20 | 21 | 22 | def format_publisher(publisher): 23 | ''' 24 | Cleans and returns the publisher name scrapped from 25 | the webpage. 26 | ''' 27 | publisher = publisher[publisher.find("by") + 3:] 28 | publisher = publisher[0:publisher.find("\n")] + " " + \ 29 | publisher[publisher.find("("):publisher.find(")") + 1] 30 | 31 | return publisher 32 | 33 | 34 | def get_book_info(book_title): 35 | ''' 36 | Searches the internet for book_title 37 | and returns a BookInfo object containing 38 | the scrapped information 39 | ''' 40 | search_txt = book_title + " book amazon india" 41 | 42 | book_amazon_link = "" 43 | for link in search(search_txt, tld="co.in", num=10, stop=5, pause=2): 44 | if "amazon.in" in link and "dp/" in link: 45 | book_amazon_link = link 46 | break 47 | 48 | isbn10 = book_amazon_link[book_amazon_link.find("dp/") + 3:] 49 | response = requests.get("https://www.goodreads.com/book/isbn/" + isbn10) 50 | soup = BeautifulSoup(response.text, "html.parser") 51 | 52 | image_url = soup.find("div", {"class": "editionCover"}).img.get("src") 53 | title = soup.find("div", {"class": "infoBoxRowItem"}).text 54 | author = soup.find("span", {"itemprop": "name"}).text 55 | publisher = soup.find_all("div", {"class": "row"})[1].text 56 | publisher = format_publisher(publisher) 57 | isbn13 = soup.find("span", {"itemprop": "isbn"}).text 58 | rating = ".".join(re.findall('\d+', soup.find("span", {"itemprop": "ratingValue"}).text)) 59 | description = soup.find(id="description").find_all("span")[1].text 60 | total_pages = soup.find("span", {"itemprop": "numberOfPages"}).text 61 | total_pages = total_pages[0:total_pages.find("pages")] 62 | genre = soup.find("a", {"class": "actionLinkLite bookPageGenreLink"}).text + \ 63 | ", " + soup.find_all("a", {"class": "actionLinkLite bookPageGenreLink"})[1].text 64 | 65 | return BookInfo( 66 | title=title, 67 | author=author, 68 | image_url=image_url, 69 | publisher=publisher, 70 | isbn_10=isbn10, 71 | isbn_13=isbn13, 72 | rating=rating, 73 | description=description, 74 | total_pages=total_pages, 75 | genre=genre 76 | ) 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bookshelf-Reader-API 2 | A browsable REST API built using Django REST Framework for recognizing book spines in an image. 3 | 4 | Uploaded Image | Result 5 | :--------------------:|:---------------------------: 6 | Uploaded Image|Resulted Image 7 | 8 | # Installation 9 | * To run this project locally, clone or download this repository. 10 | * Install requirements using: 11 | ``` 12 | pip install -r requirements.txt 13 | ``` 14 | * Then run the migrations using: 15 | ``` 16 | python3 manage.py makemigrations 17 | python3 manage.py migrate 18 | ``` 19 | * Run the application: 20 | ``` 21 | python3 manage.py runserver 22 | ``` 23 | 24 | # Usage 25 | Add these URLs after your landing URL 26 | 27 | Function | url | Return 28 | :----------------------:|:----------------------:|:----------------------------------------------------: 29 | Upload Bookshelf Image | `/api/create-bookshelf/` | ID for referring the uploaded image (Inside the Response Header) 30 | Spine Line Drawn Image | `/api/bookshelf//` | Spine line drawn image 31 | Cropped Spines | `/api/spines//` | URLS of the cropped spine images 32 | 33 | # Further Implementation 34 | 35 | This project contains scrappers to scrap the information of all the books recognized in the spine image. Recognized spine can be sent for text recognition and then the recieved text can be uploaded to below URL's for scrapping the book's information. 36 | 37 | > *NOTE 1:* It's okay if the recognized text is not accurate. The scrapper will automatically find the correct book. 38 | > *NOTE 2:* The uploaded text is expected to be the book title \[and author name\]. 39 | 40 | Function | url | Return 41 | :----------------------:|:----------------------:|:----------------------------------------------------: 42 | Upload book title text for scrapping information | `/api/add-book/` | ID of the created Book object (Inside the Response Header) 43 | Get Book Information | `/api/book//` | Scrapped book information. 44 | 45 | # Client Side Application 46 | If you want to see how this REST API can be used at client side then checkout [Bookshelf Reader Android Application](https://github.com/LakshyaKhatri/Bookshelf-Reader) 47 | 48 | # Liscence 49 | MIT License 50 | 51 | Copyright (c) 2020 Lakshya Khatri 52 | 53 | Permission is hereby granted, free of charge, to any person obtaining a copy 54 | of this software and associated documentation files (the "Software"), to deal 55 | in the Software without restriction, including without limitation the rights 56 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 57 | copies of the Software, and to permit persons to whom the Software is 58 | furnished to do so, subject to the following conditions: 59 | 60 | The above copyright notice and this permission notice shall be included in all 61 | copies or substantial portions of the Software. 62 | 63 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 64 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 65 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 66 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 67 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 68 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 69 | SOFTWARE. 70 | -------------------------------------------------------------------------------- /bookshelf_reader/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for bookshelf_reader project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.1.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = 'piuj%rb3u0)^utqd24(^wp)krvkdb8bzvt^dp8o#(fw)l1h*zc' 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | ALLOWED_HOSTS = ['*'] 28 | 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = [ 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 40 | # django apps 41 | 'detect_spines', 42 | 43 | # third party modules 44 | 'rest_framework', 45 | 'cv2', 46 | 'numpy', 47 | 'bs4', 48 | 'requests' 49 | ] 50 | 51 | MIDDLEWARE = [ 52 | 'django.middleware.security.SecurityMiddleware', 53 | 'django.contrib.sessions.middleware.SessionMiddleware', 54 | 'django.middleware.common.CommonMiddleware', 55 | 'django.middleware.csrf.CsrfViewMiddleware', 56 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 57 | 'django.contrib.messages.middleware.MessageMiddleware', 58 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 59 | ] 60 | 61 | ROOT_URLCONF = 'bookshelf_reader.urls' 62 | 63 | TEMPLATES = [ 64 | { 65 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 66 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 67 | 'APP_DIRS': True, 68 | 'OPTIONS': { 69 | 'context_processors': [ 70 | 'django.template.context_processors.debug', 71 | 'django.template.context_processors.request', 72 | 'django.contrib.auth.context_processors.auth', 73 | 'django.contrib.messages.context_processors.messages', 74 | ], 75 | }, 76 | }, 77 | ] 78 | 79 | WSGI_APPLICATION = 'bookshelf_reader.wsgi.application' 80 | 81 | 82 | # Database 83 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 84 | 85 | DATABASES = { 86 | 'default': { 87 | 'ENGINE': 'django.db.backends.sqlite3', 88 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 89 | } 90 | } 91 | 92 | 93 | # Password validation 94 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 95 | 96 | AUTH_PASSWORD_VALIDATORS = [ 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 105 | }, 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 108 | }, 109 | ] 110 | 111 | 112 | # Internationalization 113 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 114 | 115 | LANGUAGE_CODE = 'en-us' 116 | 117 | TIME_ZONE = 'UTC' 118 | 119 | USE_I18N = True 120 | 121 | USE_L10N = True 122 | 123 | USE_TZ = True 124 | 125 | 126 | # Static files (CSS, JavaScript, Images) 127 | # https://docs.djangoproject.com/en/2.1/howto/static-files/ 128 | 129 | STATIC_URL = '/static/' 130 | 131 | STATICFILES_DIRS = [ 132 | ] 133 | 134 | STATIC_ROOT = "/home/lakshya1498/Bookshelf-Reader-API/static" 135 | 136 | MEDIA_URL = "/media/" 137 | MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "static_cdn", "media_root") 138 | 139 | # Settings for Django Rest Framework 140 | 141 | REST_FRAMEWORK = { 142 | # Use Django's standard `django.contrib.auth` permissions, 143 | # or allow read-only access for unauthenticated users. 144 | 'DEFAULT_PERMISSION_CLASSES': [ 145 | 'rest_framework.permissions.AllowAny', 146 | ] 147 | } 148 | -------------------------------------------------------------------------------- /detect_spines/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | import os 3 | import spine_detection 4 | from django.core.files.base import File 5 | import scrap_book 6 | import random 7 | 8 | 9 | def bookshelf_image_path(instance, filepath): 10 | base_name = os.path.basename(filepath) 11 | name, ext = os.path.splitext(base_name) 12 | new_filename = 'bookshelf' 13 | new_filename = "{new_filename}{ext}".format( 14 | new_filename=new_filename, 15 | ext=ext 16 | ) 17 | 18 | return "bookshelfs/{new_filename}".format( 19 | new_filename=new_filename 20 | ) 21 | 22 | 23 | def spine_drawn_bookshelf_image_path(instance, filepath): 24 | base_name = os.path.basename(filepath) 25 | name, ext = os.path.splitext(base_name) 26 | new_filename = 'spine-drawn-bookshelf' 27 | final_filname = "{new_filename}{ext}".format( 28 | new_filename=new_filename, 29 | ext=ext 30 | ) 31 | 32 | return "spine-drawn-bookshelfs/{final_filname}".format( 33 | final_filname=final_filname 34 | ) 35 | 36 | 37 | def spine_image_path(instance, filepath): 38 | base_name = os.path.basename(filepath) 39 | name, ext = os.path.splitext(base_name) 40 | new_filename = 'spine' 41 | final_filname = "{new_filename}{ext}".format( 42 | new_filename=new_filename, 43 | ext=ext 44 | ) 45 | 46 | return "spines/{final_filname}".format(final_filname=final_filname) 47 | 48 | 49 | class Bookshelf(models.Model): 50 | image = models.ImageField(upload_to=bookshelf_image_path) 51 | spine_line_drawn_image = models.ImageField( 52 | upload_to=spine_drawn_bookshelf_image_path, null=True, blank=True) 53 | 54 | def save(self, *args, **kwargs): 55 | ''' 56 | Saves an image with spine lines drawn on it 57 | ''' 58 | if self.id is None: 59 | processed_image, extension = spine_detection.draw_spine_lines(self.image) 60 | self.spine_line_drawn_image.save( 61 | "image.{extension}".format(extension=extension.lower()), 62 | File(processed_image), 63 | save=False 64 | ) 65 | super(Bookshelf, self).save(*args, **kwargs) 66 | 67 | # Creates and saves cropped spines to database 68 | spine_images = spine_detection.get_spines(self.image) 69 | 70 | for spine_image in spine_images: 71 | spine = Spine.objects.create(bookshelf=self) 72 | spine.image.save( 73 | "image.{extension}".format(extension=extension.lower()), 74 | File(spine_image), 75 | save=True 76 | ) 77 | 78 | def __str__(self): 79 | return str(self.id) 80 | 81 | 82 | class Spine(models.Model): 83 | image = models.ImageField(upload_to=spine_image_path, null=True, blank=True) 84 | bookshelf = models.ForeignKey(Bookshelf, on_delete=models.CASCADE) 85 | 86 | def __str__(self): 87 | return "{book_id} : {spine_number}".format( 88 | book_id=self.bookshelf.id, 89 | spine_number=self.id 90 | ) 91 | 92 | 93 | class Book(models.Model): 94 | title = models.CharField(max_length=500, null=False) 95 | author = models.CharField(max_length=500, null=True, blank=True) 96 | price = models.CharField(max_length=20, null=True, blank=True) 97 | rating = models.CharField(max_length=20, null=True, blank=True) 98 | description = models.TextField(null=True, blank=True) 99 | publisher = models.CharField(max_length=500, null=True, blank=True) 100 | isbn_10 = models.CharField(max_length=20, null=True, blank=True) 101 | isbn_13 = models.CharField(max_length=20, null=True, blank=True) 102 | total_pages = models.CharField(max_length=10, null=True, blank=True) 103 | genre = models.CharField(max_length=500, null=True, blank=True) 104 | dimensions = models.CharField(max_length=500, null=True, blank=True) 105 | book_cover_url = models.CharField(max_length=32656232365, null=True, blank=True) 106 | 107 | def __str__(self): 108 | return "{id}. {book_title}".format(id=self.id, book_title=self.title) 109 | 110 | def save(self, *args, **kwargs): 111 | # fetch book cover image URL and isbn then save the object 112 | if self.id is None: 113 | self.title = str(self.title).title() 114 | bookInfo = scrap_book.get_book_info(self.title) 115 | self.title = bookInfo.title 116 | self.author = bookInfo.author 117 | # TODO: Add actual price 118 | self.price = "Rs. " + str(random.randint(100, 500)) + ".00" 119 | self.rating = bookInfo.rating 120 | self.description = bookInfo.description 121 | self.publisher = bookInfo.publisher 122 | self.isbn_10 = bookInfo.isbn_10 123 | self.isbn_13 = bookInfo.isbn_13 124 | self.total_pages = bookInfo.total_pages 125 | self.genre = bookInfo.genre 126 | # TODO: Add actual dimensions 127 | self.dimensions = "19.7 x 13 x 2.2 cm" 128 | self.book_cover_url = bookInfo.image_url 129 | super(Book, self).save(*args, **kwargs) 130 | -------------------------------------------------------------------------------- /spine_detection.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from PIL import Image 3 | import numpy as np 4 | import cv2 5 | import math 6 | 7 | 8 | def get_image_extension(django_image): 9 | ''' 10 | Returns image extension from a django image 11 | ''' 12 | pil_image = Image.open(django_image) 13 | return pil_image.format 14 | 15 | 16 | def opencv_image_to_django_image(opencv_image, ext): 17 | opencv_image = cv2.cvtColor(opencv_image, cv2.COLOR_BGR2RGB) 18 | django_image = BytesIO() 19 | 20 | pil_image = Image.fromarray(opencv_image) 21 | pil_image.save(django_image, format=ext) 22 | 23 | return django_image 24 | 25 | 26 | def django_image_to_opencv_image(django_image): 27 | pil_image = Image.open(django_image) 28 | img = np.array(pil_image) 29 | img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) 30 | return img 31 | 32 | 33 | def remove_duplicate_lines(sorted_points): 34 | ''' 35 | Serches for the lines that are drawn 36 | over each other in the image and returns 37 | a list of non duplicate line co-ordinates 38 | ''' 39 | last_x1 = 0 40 | non_duplicate_points = [] 41 | for point in sorted_points: 42 | ((x1, y1), (x2, y2)) = point 43 | if last_x1 == 0: 44 | non_duplicate_points.append(point) 45 | last_x1 = x1 46 | 47 | elif abs(last_x1 - x1) >= 25: 48 | non_duplicate_points.append(point) 49 | last_x1 = x1 50 | 51 | return non_duplicate_points 52 | 53 | 54 | def get_points_in_x_and_y(hough_lines, max_y): 55 | ''' 56 | Takes a list of trigonometric form of lines 57 | and returns their starting and ending 58 | co-ordinates 59 | ''' 60 | points = [] 61 | for line in hough_lines: 62 | rho, theta = line[0] 63 | a = np.cos(theta) 64 | b = np.sin(theta) 65 | x0 = a * rho 66 | y0 = b * rho 67 | x1 = int(x0 + (max_y + 100) * (-b)) 68 | y1 = int(y0 + (max_y + 100) * (a)) 69 | start = (x1, y1) 70 | 71 | x2 = int(x0 - (max_y + 100) * (-b)) 72 | y2 = int(y0 - (max_y + 100) * (a)) 73 | end = (x2, y2) 74 | 75 | points.append((start, end)) 76 | 77 | # Add a line at the very end of the image 78 | points.append(((500, max_y), (500, 0))) 79 | 80 | return points 81 | 82 | 83 | def shorten_line(points, y_max): 84 | ''' 85 | Takes a list of starting and ending 86 | co-ordinates of different lines 87 | and returns their trimmed form matching 88 | the image height 89 | ''' 90 | shortened_points = [] 91 | for point in points: 92 | ((x1, y1), (x2, y2)) = point 93 | 94 | # Slope 95 | try: 96 | m = (y2 - y1) / (x2 - x1) 97 | except ZeroDivisionError: 98 | m = -1 # Infinite slope 99 | 100 | if m == -1: 101 | shortened_points.append(((x1, y_max), (x1, 0))) 102 | continue 103 | 104 | # From equation of line: 105 | # y-y1 = m (x-x1) 106 | # x = (y-y1)/m + x1 107 | # let y = y_max 108 | new_x1 = math.ceil(((y_max - y1) / m) + x1) 109 | start_point = (abs(new_x1), y_max) 110 | 111 | # Now let y = 0 112 | new_x2 = math.ceil(((0 - y1) / m) + x1) 113 | end_point = (abs(new_x2), 0) 114 | 115 | shortened_points.append((start_point, end_point)) 116 | 117 | return shortened_points 118 | 119 | 120 | def get_cropped_images(image, points): 121 | ''' 122 | Takes a spine line drawn image and 123 | returns a list of opencv images splitted 124 | from the drawn lines 125 | ''' 126 | image = image.copy() 127 | y_max, _, _ = image.shape 128 | last_x1 = 0 129 | last_x2 = 0 130 | cropped_images = [] 131 | 132 | for point in points: 133 | ((x1, y1), (x2, y2)) = point 134 | 135 | crop_points = np.array([[last_x1, y_max], 136 | [last_x2, 0], 137 | [x2, y2], 138 | [x1, y1]]) 139 | 140 | # Crop the bounding rect 141 | rect = cv2.boundingRect(crop_points) 142 | x, y, w, h = rect 143 | cropped = image[y: y + h, x: x + w].copy() 144 | 145 | # make mask 146 | crop_points = crop_points - crop_points.min(axis=0) 147 | mask = np.zeros(cropped.shape[:2], np.uint8) 148 | cv2.drawContours(mask, [crop_points], -1, (255, 255, 255), -1, cv2.LINE_AA) 149 | 150 | # do bit-op 151 | dst = cv2.bitwise_and(cropped, cropped, mask=mask) 152 | cropped_images.append(dst) 153 | 154 | last_x1 = x1 155 | last_x2 = x2 156 | 157 | return cropped_images 158 | 159 | 160 | def resize_img(img): 161 | img = img.copy() 162 | img_ht, img_wd, _ = img.shape 163 | ratio = img_wd / img_ht 164 | new_width = 500 165 | new_height = math.ceil(new_width / ratio) 166 | resized_image = cv2.resize(img, (new_width, new_height)) 167 | 168 | return resized_image 169 | 170 | 171 | def detect_spines(img): 172 | ''' 173 | Returns a list of lines seperating 174 | the detected spines in the image 175 | ''' 176 | img = img.copy() 177 | height, width, _ = img.shape 178 | 179 | blur = cv2.GaussianBlur(img, (5, 5), 0) 180 | 181 | gray = cv2.cvtColor(blur, cv2.COLOR_BGR2GRAY) 182 | 183 | edge = cv2.Canny(gray, 50, 70) 184 | 185 | # kernel = np.ones((4, 1), np.uint8) 186 | kernel = np.array([[0, 0, 0, 0, 1, 0, 0, 0, 0], 187 | [0, 0, 0, 0, 1, 0, 0, 0, 0], 188 | [0, 0, 0, 0, 1, 0, 0, 0, 0], 189 | [0, 0, 0, 0, 1, 0, 0, 0, 0], 190 | [0, 0, 0, 0, 1, 0, 0, 0, 0]], dtype=np.uint8) 191 | 192 | img_erosion = cv2.erode(edge, kernel, iterations=1) 193 | 194 | lines = cv2.HoughLines(img_erosion, 1, np.pi / 180, 100) 195 | if lines is None: 196 | return [] 197 | points = get_points_in_x_and_y(lines, height) 198 | points.sort(key=lambda val: val[0][0]) 199 | non_duplicate_points = remove_duplicate_lines(points) 200 | 201 | final_points = shorten_line(non_duplicate_points, height) 202 | 203 | return final_points 204 | 205 | 206 | def get_spines(django_image): 207 | img = django_image_to_opencv_image(django_image) 208 | ext = get_image_extension(django_image) 209 | 210 | final_image = resize_img(img) 211 | final_points = detect_spines(final_image) 212 | cropped_images = get_cropped_images(final_image, final_points) 213 | 214 | django_cropped_images = [] 215 | for cropped_image in cropped_images: 216 | django_cropped_images.append( 217 | opencv_image_to_django_image( 218 | cropped_image, 219 | ext 220 | ) 221 | ) 222 | 223 | return django_cropped_images 224 | 225 | 226 | def draw_spine_lines(django_image): 227 | img = django_image_to_opencv_image(django_image) 228 | ext = get_image_extension(django_image) 229 | 230 | final_image = resize_img(img) 231 | final_points = detect_spines(final_image) 232 | 233 | for point in final_points: 234 | ((x1, y1), (x2, y2)) = point 235 | final_image = cv2.line(final_image, (x1, y1), (x2, y2), (0, 0, 255), 10) 236 | 237 | django_image = opencv_image_to_django_image( 238 | final_image, 239 | ext 240 | ) 241 | return django_image, ext 242 | --------------------------------------------------------------------------------