You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

421 lines
14 KiB

  1. package com.sothr.imagetools
  2. import java.io.File
  3. import akka.actor._
  4. import akka.routing.{Broadcast, RoundRobinRouter, SmallestMailboxRouter}
  5. import akka.pattern.ask
  6. import akka.util.Timeout
  7. import java.util.concurrent.TimeUnit
  8. import com.sothr.imagetools.image.{SimilarImages, Image}
  9. import com.sothr.imagetools.hash.HashService
  10. import com.sothr.imagetools.util.{PropertiesEnum, PropertiesService}
  11. import scala.concurrent.Await
  12. import java.lang.Thread
  13. import scala.collection.mutable
  14. import akka.routing.Broadcast
  15. class ConcurrentEngine extends Engine with grizzled.slf4j.Logging {
  16. val engineProcessingController = system.actorOf(Props[ConcurrentEngineProcessingController], name = "EngineProcessingController")
  17. val engineSimilarityController = system.actorOf(Props[ConcurrentEngineSimilarityController], name = "EngineSimilarityController")
  18. implicit val timeout = Timeout(30, TimeUnit.SECONDS)
  19. override def setProcessedListener(listenerRef: ActorRef) = {
  20. engineProcessingController ! SetNewListener(listenerRef)
  21. }
  22. override def setSimilarityListener(listenerRef: ActorRef) = {
  23. engineSimilarityController ! SetNewListener(listenerRef)
  24. }
  25. def getImagesForDirectory(directoryPath:String, recursive:Boolean=false, recursiveDepth:Int=500):List[Image] = {
  26. debug(s"Looking for images in directory: $directoryPath")
  27. val imageFiles = getAllImageFiles(directoryPath, recursive, recursiveDepth)
  28. val images:mutable.MutableList[Image] = new mutable.MutableList[Image]()
  29. for (file <- imageFiles) {
  30. engineProcessingController ! EngineProcessFile(file)
  31. }
  32. engineProcessingController ! EngineNoMoreFiles
  33. var doneProcessing = false
  34. while(!doneProcessing) {
  35. val f = engineProcessingController ? EngineIsProcessingFinished
  36. val result = Await.result(f, timeout.duration).asInstanceOf[Boolean]
  37. result match {
  38. case true =>
  39. doneProcessing = true
  40. debug("Processing Complete")
  41. case false =>
  42. debug("Still Processing")
  43. //sleep thread
  44. Thread.sleep(5000L)
  45. //val future = Future { blocking(Thread.sleep(5000L)); "done" }
  46. }
  47. }
  48. val f = engineProcessingController ? EngineGetProcessingResults
  49. val result = Await.result(f, timeout.duration).asInstanceOf[List[Image]]
  50. images ++= result
  51. images.toList
  52. }
  53. //needs to be rebuilt
  54. def getSimilarImagesForDirectory(directoryPath:String, recursive:Boolean=false, recursiveDepth:Int=500):List[SimilarImages] = {
  55. debug(s"Looking for similar images in directory: $directoryPath")
  56. val images = getImagesForDirectory(directoryPath, recursive, recursiveDepth)
  57. info(s"Searching ${images.length} images for similarities")
  58. for (rootImage <- images) {
  59. debug(s"Looking for images similar to: ${rootImage.imagePath}")
  60. engineSimilarityController ! EngineCompareImages(rootImage, images)
  61. }
  62. //tell the comparison engine there's nothing left to compare
  63. engineSimilarityController ! EngineNoMoreComparisons
  64. var doneProcessing = false
  65. while(!doneProcessing) {
  66. val f = engineSimilarityController ? EngineIsSimilarityFinished
  67. val result = Await.result(f, timeout.duration).asInstanceOf[Boolean]
  68. result match {
  69. case true =>
  70. doneProcessing = true
  71. debug("Processing Complete")
  72. case false =>
  73. debug("Still Processing")
  74. //sleep thread
  75. Thread.sleep(5000L)
  76. //val future = Future { blocking(Thread.sleep(5000L)); "done" }
  77. }
  78. }
  79. val f = engineSimilarityController ? EngineGetSimilarityResults
  80. val result = Await.result(f, timeout.duration).asInstanceOf[List[SimilarImages]]
  81. //process the result into a list we want in cleanedSimilarImages
  82. var count = 0
  83. val cleanedSimilarImages = new mutable.MutableList[SimilarImages]()
  84. val ignoreSet = new mutable.HashSet[Image]()
  85. for (similarImages <- result) {
  86. count += 1
  87. if (count % 25 == 0 || count == result.length) debug(s"Cleaning similar image $count/${result.length} ${result.length - count} left to clean")
  88. if (!ignoreSet.contains(similarImages.rootImage)) {
  89. cleanedSimilarImages += similarImages
  90. ignoreSet += similarImages.rootImage
  91. for (image <- similarImages.similarImages) {
  92. ignoreSet += image
  93. }
  94. }
  95. }
  96. var similarCount = 0
  97. for (similarImage <- cleanedSimilarImages) {
  98. similarCount += 1 + similarImage.similarImages.size
  99. }
  100. info(s"Finished processing ${images.size} images. Found $similarCount similar images")
  101. cleanedSimilarImages.toList
  102. }
  103. }
  104. // external cases //
  105. case class SetNewListener(listenerType: ActorRef)
  106. // processing files into images
  107. case class EngineProcessFile(file:File)
  108. case object EngineNoMoreFiles
  109. case object EngineIsProcessingFinished
  110. case object EngineGetProcessingResults
  111. //internal cases
  112. case class EngineFileProcessed(image:Image)
  113. case object EngineActorProcessingFinished
  114. case object EngineActorReactivate
  115. class ConcurrentEngineProcessingController extends Actor with ActorLogging {
  116. val numOfRouters = {
  117. val max = PropertiesService.get(PropertiesEnum.ConcurrentProcessingLimit.toString).toInt
  118. val processors = Runtime.getRuntime.availableProcessors()
  119. var threads = 0
  120. if (processors > max) threads = max else if (processors > 1) threads = processors - 1 else threads = 1
  121. threads
  122. }
  123. val router = context.actorOf(Props[ConcurrentEngineProcessingActor].withRouter(SmallestMailboxRouter(nrOfInstances = numOfRouters)))
  124. var images:mutable.MutableList[Image] = new mutable.MutableList[Image]()
  125. var toProcess = 0
  126. var processed = 0
  127. var processorsFinished = 0
  128. var listener = context.actorOf(Props[DefaultLoggingEngineListener],
  129. name = "ProcessedEngineListener")
  130. def setListener(newListener: ActorRef) = {
  131. //remove the old listener
  132. this.listener ! PoisonPill
  133. //setup the new listener
  134. this.listener = newListener
  135. }
  136. override def preStart() = {
  137. log.info("Staring the controller for processing images")
  138. log.info("Using {} actors to process images", numOfRouters)
  139. }
  140. override def receive = {
  141. case command:SetNewListener => setListener(command.listenerType)
  142. case command:EngineProcessFile => processFile(command)
  143. case command:EngineFileProcessed => fileProcessed(command)
  144. case EngineNoMoreFiles => requestWrapup()
  145. case EngineActorProcessingFinished => actorProcessingFinished()
  146. case EngineIsProcessingFinished => isProcessingFinished()
  147. case EngineGetProcessingResults => getResults()
  148. case _ => log.info("received unknown message")
  149. }
  150. override def postStop() = {
  151. super.postStop()
  152. this.listener ! PoisonPill
  153. }
  154. def processFile(command:EngineProcessFile) = {
  155. log.debug(s"Started evaluating ${command.file.getAbsolutePath}")
  156. toProcess += 1
  157. router ! command
  158. }
  159. def fileProcessed(command:EngineFileProcessed) = {
  160. processed += 1
  161. if (processed % 25 == 0 || processed == toProcess) {
  162. //log.info(s"Processed $processed/$toProcess")
  163. listener ! ComparedFileCount(processed,toProcess)
  164. }
  165. if (command.image != null) {
  166. log.debug(s"processed image: ${command.image.imagePath}")
  167. images += command.image
  168. }
  169. }
  170. def requestWrapup() = {
  171. router ! Broadcast(EngineNoMoreFiles)
  172. }
  173. /*
  174. * Record that a processor is done
  175. */
  176. def actorProcessingFinished() = {
  177. processorsFinished += 1
  178. }
  179. /*
  180. * Check if processing is done
  181. */
  182. def isProcessingFinished() = {
  183. try {
  184. if (processorsFinished >= numOfRouters) sender ! true else sender ! false
  185. } catch {
  186. case e: Exception
  187. sender ! akka.actor.Status.Failure(e)
  188. throw e
  189. }
  190. }
  191. /*
  192. * Get the results of the processing
  193. */
  194. def getResults() = {
  195. try {
  196. processorsFinished = 0
  197. toProcess = 0
  198. processed = 0
  199. sender ! images.toList
  200. images.clear()
  201. } catch {
  202. case e: Exception
  203. sender ! akka.actor.Status.Failure(e)
  204. throw e
  205. }
  206. }
  207. }
  208. class ConcurrentEngineProcessingActor extends Actor with ActorLogging {
  209. var ignoreMessages = false
  210. override def receive = {
  211. case command:EngineProcessFile => processFile(command)
  212. case EngineNoMoreFiles => finishedProcessingFiles()
  213. case EngineActorReactivate => ignoreMessages = false
  214. case _ => log.info("received unknown message")
  215. }
  216. def processFile(command:EngineProcessFile) = {
  217. if (!ignoreMessages) {
  218. val image = ImageService.getImage(command.file)
  219. if (image != null) {
  220. sender ! EngineFileProcessed(image)
  221. } else {
  222. log.debug(s"Failed to process image: ${command.file.getAbsolutePath}")
  223. }
  224. }
  225. }
  226. def finishedProcessingFiles() = {
  227. if (!ignoreMessages) {
  228. ignoreMessages = true
  229. sender ! EngineActorProcessingFinished
  230. }
  231. }
  232. }
  233. //finding similarities between images
  234. case class EngineCompareImages(image1:Image, images:List[Image])
  235. case class EngineCompareImagesComplete(similarImages:SimilarImages)
  236. case object EngineNoMoreComparisons
  237. case object EngineIsSimilarityFinished
  238. case object EngineGetSimilarityResults
  239. case object EngineActorCompareImagesFinished
  240. class ConcurrentEngineSimilarityController extends Actor with ActorLogging {
  241. val numOfRouters = {
  242. val max = PropertiesService.get(PropertiesEnum.ConcurrentSimiliartyLimit.toString).toInt
  243. val processors = Runtime.getRuntime.availableProcessors()
  244. var threads = 0
  245. if (processors > max) threads = max else if (processors > 1) threads = processors - 1 else threads = 1
  246. threads
  247. }
  248. val router = context.actorOf(Props[ConcurrentEngineSimilarityActor].withRouter(RoundRobinRouter(nrOfInstances = numOfRouters)))
  249. val allSimilarImages = new mutable.MutableList[SimilarImages]
  250. var toProcess = 0
  251. var processed = 0
  252. var processorsFinished = 0
  253. var listener = context.actorOf(Props[DefaultLoggingEngineListener],
  254. name = "SimilarityEngineListener")
  255. def setListener(newListener: ActorRef) = {
  256. //remove the old listener
  257. this.listener ! PoisonPill
  258. //setup the new listener
  259. this.listener = newListener
  260. }
  261. override def preStart() = {
  262. log.info("Staring the controller for processing similarities between images")
  263. log.info("Using {} actors to process image similarities", numOfRouters)
  264. }
  265. override def receive = {
  266. case command:SetNewListener => setListener(command.listenerType)
  267. case command:EngineCompareImages => findSimilarities(command)
  268. case command:EngineCompareImagesComplete => similarityProcessed(command)
  269. case EngineNoMoreComparisons => requestWrapup()
  270. case EngineActorCompareImagesFinished => actorProcessingFinished()
  271. case EngineIsSimilarityFinished => isProcessingFinished()
  272. case EngineGetSimilarityResults => getResults()
  273. case _ => log.info("received unknown message")
  274. }
  275. override def postStop() = {
  276. super.postStop()
  277. this.listener ! PoisonPill
  278. }
  279. def findSimilarities(command:EngineCompareImages) = {
  280. log.debug(s"Finding similarities between {} and {} images", command.image1.imagePath, command.images.length)
  281. toProcess += 1
  282. if (toProcess % 250 == 0) {
  283. //log.info("Sent {} images to be processed for similarites", toProcess)
  284. listener ! SubmitMessage(s"Sent $toProcess images to be processed for similarites")
  285. }
  286. //just relay the command to our workers
  287. router ! command
  288. }
  289. def similarityProcessed(command:EngineCompareImagesComplete) = {
  290. processed += 1
  291. if (processed % 25 == 0 || processed == toProcess) {
  292. //log.info(s"Processed $processed/$toProcess")
  293. listener ! ScannedFileCount(processed,toProcess)
  294. }
  295. if (command.similarImages != null) {
  296. log.debug(s"Found similar images: ${command.similarImages}")
  297. allSimilarImages += command.similarImages
  298. }
  299. }
  300. def requestWrapup() = {
  301. router ! Broadcast(EngineNoMoreComparisons)
  302. }
  303. /*
  304. * Record that a processor is done
  305. */
  306. def actorProcessingFinished() = {
  307. processorsFinished += 1
  308. log.debug("Similarity Processor Reported Finished")
  309. }
  310. /*
  311. * Check if processing is done
  312. */
  313. def isProcessingFinished() = {
  314. try {
  315. log.debug("Processors Finished {}/{}", processorsFinished, numOfRouters)
  316. if (processorsFinished >= numOfRouters) sender ! true else sender ! false
  317. } catch {
  318. case e: Exception
  319. sender ! akka.actor.Status.Failure(e)
  320. throw e
  321. }
  322. }
  323. /*
  324. * Get the results of the processing
  325. */
  326. def getResults() = {
  327. try {
  328. processorsFinished = 0
  329. toProcess = 0
  330. processed = 0
  331. sender ! allSimilarImages.toList
  332. allSimilarImages.clear()
  333. } catch {
  334. case e: Exception
  335. sender ! akka.actor.Status.Failure(e)
  336. throw e
  337. }
  338. }
  339. }
  340. class ConcurrentEngineSimilarityActor extends Actor with ActorLogging {
  341. var ignoreMessages = false
  342. override def receive = {
  343. case command:EngineCompareImages => compareImages(command)
  344. case EngineNoMoreComparisons => finishedComparisons()
  345. case EngineActorReactivate => ignoreMessages = false
  346. case _ => log.info("received unknown message")
  347. }
  348. def compareImages(command:EngineCompareImages) = {
  349. if (!ignoreMessages) {
  350. val similarImages = new mutable.MutableList[Image]()
  351. for (image <- command.images) {
  352. if (!command.image1.equals(image)) {
  353. if (HashService.areImageHashesSimilar(command.image1.hashes, image.hashes)) {
  354. similarImages += image
  355. }
  356. }
  357. }
  358. //only send a message if we find similar images
  359. if (similarImages.length >= 1) {
  360. val similarImage = new SimilarImages(command.image1, similarImages.toList)
  361. log.debug(s"Found ${similarImage.similarImages.length} similar images to ${similarImage.rootImage}")
  362. sender ! EngineCompareImagesComplete(similarImage)
  363. } else {
  364. log.debug(s"Found no similar images to ${command.image1}")
  365. sender ! EngineCompareImagesComplete(null)
  366. }
  367. }
  368. }
  369. def finishedComparisons() = {
  370. if (!ignoreMessages) {
  371. ignoreMessages = true
  372. log.debug("Finished processing comparisons")
  373. sender ! EngineActorCompareImagesFinished
  374. }
  375. }
  376. }