Project.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import os
  2. import shutil
  3. import typing as T
  4. from datetime import datetime
  5. from eventlet import tpool
  6. from pycs import app
  7. from pycs import db
  8. from pycs.database.base import NamedBaseModel
  9. from pycs.database.Collection import Collection
  10. from pycs.database.File import File
  11. from pycs.database.Label import Label
  12. from pycs.database.util import commit_on_return
  13. from pycs.util.FileOperations import resize_file
  14. class Project(NamedBaseModel):
  15. """ DB Model for projects """
  16. description = db.Column(db.String)
  17. created = db.Column(db.DateTime, default=datetime.utcnow,
  18. index=True, nullable=False)
  19. model_id = db.Column(
  20. db.Integer,
  21. db.ForeignKey("model.id", ondelete="SET NULL"))
  22. label_provider_id = db.Column(
  23. db.Integer,
  24. db.ForeignKey("label_provider.id", ondelete="SET NULL"))
  25. root_folder = db.Column(db.String, nullable=False, unique=True)
  26. external_data = db.Column(db.Boolean, nullable=False)
  27. data_folder = db.Column(db.String, nullable=False)
  28. # contraints
  29. __table_args__ = ()
  30. # relationships to other models
  31. files = db.relationship(
  32. "File",
  33. backref="project",
  34. lazy="dynamic",
  35. passive_deletes=True,
  36. )
  37. labels = db.relationship(
  38. "Label",
  39. backref="project",
  40. lazy="dynamic",
  41. passive_deletes=True,
  42. )
  43. collections = db.relationship(
  44. "Collection",
  45. backref="project",
  46. lazy="dynamic",
  47. passive_deletes=True,
  48. )
  49. serialize_only = NamedBaseModel.serialize_only + (
  50. "created",
  51. "description",
  52. "model_id",
  53. "label_provider_id",
  54. "root_folder",
  55. "external_data",
  56. "data_folder",
  57. )
  58. @commit_on_return
  59. def delete(self) -> T.Tuple[dict, dict]:
  60. # pylint: disable=unexpected-keyword-arg
  61. dump = super().delete(commit=False)
  62. model_dump = {}
  63. if self.model_id is not None:
  64. # pylint: disable=unexpected-keyword-arg
  65. model_dump = self.model.delete(commit=False)
  66. if os.path.exists(self.root_folder):
  67. # remove from file system
  68. shutil.rmtree(self.root_folder)
  69. return dump, model_dump
  70. def label(self, identifier: int) -> T.Optional[Label]:
  71. """
  72. get a label using its unique identifier
  73. :param identifier: unique identifier
  74. :return: label
  75. """
  76. return self.labels.filter(Label.id == identifier).one_or_none()
  77. def label_by_reference(self, reference: str) -> T.Optional[Label]:
  78. """
  79. get a label using its reference string
  80. :param reference: reference string
  81. :return: label
  82. """
  83. return self.labels.filter(Label.reference == reference).one_or_none()
  84. def file(self, identifier: int) -> T.Optional[File]:
  85. """
  86. get a file using its unique identifier
  87. :param identifier: unique identifier
  88. :return: file
  89. """
  90. return self.files.filter(File.id == identifier).one_or_none()
  91. def collection(self, identifier: int) -> T.Optional[Collection]:
  92. """
  93. get a collection using its unique identifier
  94. :param identifier: unique identifier
  95. :return: collection
  96. """
  97. return self.collections.filter(Collection.id == identifier).one_or_none()
  98. def collection_by_reference(self, reference: str) -> T.Optional[Collection]:
  99. """
  100. get a collection using its unique identifier
  101. :param identifier: unique identifier
  102. :return: collection
  103. """
  104. return self.collections.filter(Collection.reference == reference).one_or_none()
  105. @commit_on_return
  106. def create_label(self, name: str,
  107. reference: str = None,
  108. parent: T.Optional[T.Union[int, str, Label]] = None,
  109. hierarchy_level: str = None) -> T.Tuple[T.Optional[Label], bool]:
  110. """
  111. create a label for this project. If there is already a label with the same reference
  112. in the database its name is updated.
  113. :param name: label name
  114. :param reference: label reference
  115. :param parent: parent label. Either a reference string, a Label id or a Label instance
  116. :param hierarchy_level: hierarchy level name
  117. :return: created or edited label, insert
  118. """
  119. label = None
  120. is_new = False
  121. if reference is not None:
  122. label = Label.query.filter_by(project_id=self.id, reference=reference).one_or_none()
  123. if label is None:
  124. label = Label.new(commit=False, project_id=self.id, reference=reference)
  125. is_new = True
  126. label.set_name(name, commit=False)
  127. label.set_parent(parent, commit=False)
  128. label.hierarchy_level = hierarchy_level
  129. return label, is_new
  130. @commit_on_return
  131. def bulk_create_labels(self, labels: T.List[T.Dict], clean_old_labels: bool = True):
  132. """
  133. Inserts a all labels at once.
  134. :raises:
  135. - AssertionError if project_id and reference are not unique
  136. - ValueError if a cycle in the hierarchy is found
  137. """
  138. if len(labels) == 0:
  139. return labels
  140. if clean_old_labels:
  141. self.labels.delete()
  142. for label in labels:
  143. label["project_id"] = self.id
  144. self.__check_labels(labels)
  145. app.logger.info(f"Inserting {len(labels):,d} labels")
  146. db.engine.execute(Label.__table__.insert(), labels)
  147. self.__set_parents(labels)
  148. return labels
  149. def __set_parents(self, labels):
  150. """ after the bul insert, we need to set correct parent_ids """
  151. app.logger.info("Setting parents of the labels")
  152. self.flush()
  153. for label in labels:
  154. if label["parent"] is None:
  155. continue
  156. label_obj = self.label_by_reference(label["reference"])
  157. parent_label_obj = self.label_by_reference(label["parent"])
  158. label_obj.parent_id = parent_label_obj.id
  159. # pylint: disable=no-self-use
  160. def __check_labels(self, labels):
  161. """ check labels for unique keys and cycles """
  162. unique_keys = {}
  163. for label in labels:
  164. key = (label["project_id"], label["reference"])
  165. assert key not in unique_keys, \
  166. f"{key} was not unique: ({label=} vs {unique_keys[key]=})!"
  167. unique_keys[key] = label
  168. # pylint: disable=too-many-arguments
  169. @commit_on_return
  170. def create_collection(self,
  171. reference: str,
  172. name: str,
  173. description: str,
  174. position: int,
  175. autoselect: bool) -> T.Tuple[Collection, bool]:
  176. """
  177. create a new collection associated with this project
  178. :param reference: collection reference string
  179. :param name: collection name
  180. :param description: collection description
  181. :param position: position in menus
  182. :param autoselect: automatically select this collection on session load
  183. :return: collection object, insert
  184. """
  185. collection, is_new = Collection.get_or_create(
  186. project_id=self.id, reference=reference)
  187. collection.name = name
  188. collection.description = description
  189. collection.position = position
  190. collection.autoselect = autoselect
  191. return collection, is_new
  192. # pylint: disable=too-many-arguments
  193. @commit_on_return
  194. def add_file(self,
  195. uuid: str,
  196. file_type: str,
  197. name: str,
  198. extension: str,
  199. size: int,
  200. filename: str,
  201. frames: int = None,
  202. fps: float = None) -> T.Tuple[File, bool]:
  203. """
  204. add a file to this project
  205. :param uuid: unique identifier which is used for temporary files
  206. :param file_type: file type (either image or video)
  207. :param name: file name
  208. :param extension: file extension
  209. :param size: file size
  210. :param filename: actual name in filesystem
  211. :param frames: frame count
  212. :param fps: frames per second
  213. :return: file
  214. """
  215. path = os.path.join(self.data_folder, f"{filename}{extension}")
  216. file, is_new = File.get_or_create(
  217. project_id=self.id, path=path)
  218. file.uuid = uuid
  219. file.type = file_type
  220. file.name = name
  221. file.extension = extension
  222. file.size = size
  223. file.frames = frames
  224. file.fps = fps
  225. # Pre-load common thumbnail sizes if the given file is an image.
  226. if file.type == 'image' and os.path.isfile(path):
  227. for max_width, max_height in [(200, 200), (2000, 800)]:
  228. tpool.execute(resize_file, file, self.root_folder, max_width, max_height)
  229. return file, is_new
  230. def get_files(self, *filters, offset: int = 0, limit: int = -1,
  231. with_annotations: T.Optional[bool] = None) -> T.List[File]:
  232. """
  233. get an iterator of files associated with this project
  234. :param offset: file offset
  235. :param limit: file limit
  236. :return: iterator of files
  237. """
  238. if with_annotations is not None:
  239. annot_query = File.results.any()
  240. if with_annotations == False:
  241. annot_query = ~annot_query
  242. filters = filters + (annot_query,)
  243. return self.files.filter(*filters).order_by(File.path).offset(offset).limit(limit)
  244. def _files_without_results(self):
  245. """
  246. get files without any results
  247. :return: a query object
  248. """
  249. # pylint: disable=no-member
  250. return self.files.filter(~File.results.any())
  251. def count_files_without_results(self) -> int:
  252. """
  253. count files without associated results
  254. :return: count
  255. """
  256. return self._files_without_results().count()
  257. def files_without_results(self) -> T.List[File]:
  258. """
  259. get a list of files without associated results
  260. :return: list of files
  261. """
  262. return self._files_without_results().all()
  263. def _files_without_collection(self, offset: int = 0, limit: int = -1):
  264. """
  265. get files without a collection
  266. :return: a query object
  267. """
  268. # pylint: disable=no-member
  269. return self.get_files(File.collection_id.is_(None), offset=offset, limit=limit)
  270. def files_without_collection(self, offset: int = 0, limit: int = -1) -> T.List[File]:
  271. """
  272. get a list of files without a collection
  273. :return: list of files
  274. """
  275. return self._files_without_collection(offset=offset, limit=limit).all()
  276. def count_files_without_collection(self) -> int:
  277. """
  278. count files associated with this project but without a collection
  279. :return: count
  280. """
  281. return self._files_without_collection().count()