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.

449 lines
14 KiB

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