001/** 002 * Copyright (c) 2011, The University of Southampton and the individual contributors. 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without modification, 006 * are permitted provided that the following conditions are met: 007 * 008 * * Redistributions of source code must retain the above copyright notice, 009 * this list of conditions and the following disclaimer. 010 * 011 * * Redistributions in binary form must reproduce the above copyright notice, 012 * this list of conditions and the following disclaimer in the documentation 013 * and/or other materials provided with the distribution. 014 * 015 * * Neither the name of the University of Southampton nor the names of its 016 * contributors may be used to endorse or promote products derived from this 017 * software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030package org.openimaj.image.dataset; 031 032import java.io.IOException; 033import java.io.InputStream; 034import java.net.URL; 035import java.util.ArrayList; 036import java.util.Iterator; 037import java.util.List; 038import java.util.regex.Matcher; 039import java.util.regex.Pattern; 040 041import javax.xml.parsers.ParserConfigurationException; 042 043import org.openimaj.data.dataset.ReadableListDataset; 044import org.openimaj.image.Image; 045import org.openimaj.io.HttpUtils; 046import org.openimaj.io.InputStreamObjectReader; 047import org.openimaj.util.api.auth.common.FlickrAPIToken; 048 049import com.flickr4java.flickr.Flickr; 050import com.flickr4java.flickr.REST; 051import com.flickr4java.flickr.collections.Collection; 052import com.flickr4java.flickr.collections.CollectionsInterface; 053import com.flickr4java.flickr.galleries.Gallery; 054import com.flickr4java.flickr.photos.Extras; 055import com.flickr4java.flickr.photos.Photo; 056import com.flickr4java.flickr.photos.PhotoList; 057import com.flickr4java.flickr.photosets.PhotosetsInterface; 058 059/** 060 * Class to dynamically create image datasets from flickr through various api 061 * calls. 062 * 063 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 064 * 065 * @param <IMAGE> 066 * The type of {@link Image} instance held by the dataset. 067 */ 068public class FlickrImageDataset<IMAGE extends Image<?, IMAGE>> extends ReadableListDataset<IMAGE, InputStream> { 069 /** 070 * Possible sizes of image from flickr. 071 * 072 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 073 */ 074 public enum Size { 075 /** 076 * The original uploaded size 077 */ 078 Original { 079 @Override 080 protected URL getURL(Photo photo) { 081 try { 082 return new URL(photo.getOriginalUrl()); 083 } catch (final Exception e) { 084 throw new RuntimeException(e); 085 } 086 } 087 }, 088 /** 089 * Large size 090 */ 091 Large { 092 @Override 093 protected URL getURL(Photo photo) { 094 try { 095 return new URL(photo.getLargeUrl()); 096 } catch (final Exception e) { 097 throw new RuntimeException(e); 098 } 099 } 100 }, 101 /** 102 * Medium size 103 */ 104 Medium { 105 @Override 106 protected URL getURL(Photo photo) { 107 try { 108 return new URL(photo.getMediumUrl()); 109 } catch (final Exception e) { 110 throw new RuntimeException(e); 111 } 112 } 113 }, 114 /** 115 * Small size 116 */ 117 Small { 118 @Override 119 protected URL getURL(Photo photo) { 120 try { 121 return new URL(photo.getSmallUrl()); 122 } catch (final Exception e) { 123 throw new RuntimeException(e); 124 } 125 } 126 }, 127 /** 128 * Thumbnail size 129 */ 130 Thumbnail { 131 @Override 132 protected URL getURL(Photo photo) { 133 try { 134 return new URL(photo.getThumbnailUrl()); 135 } catch (final Exception e) { 136 throw new RuntimeException(e); 137 } 138 } 139 }, 140 /** 141 * Square thumbnail size 142 */ 143 Square { 144 @Override 145 protected URL getURL(Photo photo) { 146 try { 147 return new URL(photo.getSmallSquareUrl()); 148 } catch (final Exception e) { 149 throw new RuntimeException(e); 150 } 151 } 152 }; 153 154 protected abstract URL getURL(Photo photo); 155 } 156 157 private final static Pattern GALLERY_URL_PATTERN = Pattern.compile(".*/photos/.*/galleries/[0-9]*(/|$)"); 158 private final static Pattern PHOTOSET_URL_PATTERN = Pattern.compile(".*/photos/.*/sets/([0-9]*)(/|$)"); 159 private final static Pattern COLLECTION_URL_PATTERN = Pattern.compile(".*/photos/(.*)/collections/([0-9]*)(/|$)"); 160 161 protected List<Photo> photos; 162 protected Size targetSize = Size.Medium; 163 164 protected FlickrImageDataset(InputStreamObjectReader<IMAGE> reader, List<Photo> photos) { 165 super(reader); 166 167 this.photos = photos; 168 } 169 170 /** 171 * Set the size of the images that this dataset produces. 172 * 173 * @param size 174 * the size 175 */ 176 public void setImageSize(Size size) { 177 this.targetSize = size; 178 } 179 180 /** 181 * Get the size of the images that this dataset produces. 182 * 183 * @return the size of the returned images 184 */ 185 public Size getImageSize() { 186 return targetSize; 187 } 188 189 /** 190 * Get the underlying flickr {@link Photo} objects. 191 * 192 * @return the underlying list of {@link Photo}s. 193 */ 194 public List<Photo> getPhotos() { 195 return photos; 196 } 197 198 /** 199 * Get the a specific underlying flickr {@link Photo} object corresponding 200 * to a particular image instance. 201 * 202 * @param index 203 * the index of the instance 204 * 205 * @return the underlying {@link Photo} corresponding to the given instance 206 * index. 207 */ 208 public Photo getPhoto(int index) { 209 return photos.get(index); 210 } 211 212 @Override 213 public IMAGE getInstance(int index) { 214 return read(photos.get(index)); 215 } 216 217 @Override 218 public int numInstances() { 219 return photos.size(); 220 } 221 222 @Override 223 public String getID(int index) { 224 return targetSize.getURL(photos.get(index)).toString(); 225 } 226 227 private IMAGE read(Photo next) { 228 if (next == null) 229 return null; 230 231 InputStream stream = null; 232 try { 233 stream = HttpUtils.readURL(targetSize.getURL(next)); 234 235 return reader.read(stream); 236 } catch (final IOException e) { 237 throw new RuntimeException(e); 238 } finally { 239 try { 240 if (stream != null) 241 stream.close(); 242 } catch (final IOException e) { 243 // ignore 244 } 245 } 246 } 247 248 @Override 249 public Iterator<IMAGE> iterator() { 250 return new Iterator<IMAGE>() { 251 Iterator<Photo> internal = photos.iterator(); 252 253 @Override 254 public boolean hasNext() { 255 return internal.hasNext(); 256 } 257 258 @Override 259 public IMAGE next() { 260 return read(internal.next()); 261 } 262 263 @Override 264 public void remove() { 265 internal.remove(); 266 } 267 }; 268 } 269 270 @Override 271 public String toString() { 272 return String.format("%s(%d images)", this.getClass().getName(), this.photos.size()); 273 } 274 275 /** 276 * Create an image dataset from the flickr gallery, photoset or collection 277 * at the given url. 278 * 279 * @param reader 280 * the reader with which to load the images 281 * @param token 282 * the flickr api authentication token 283 * @param url 284 * the url of the collection/gallery/photo-set 285 * @return a {@link FlickrImageDataset} created from the given url 286 * @throws Exception 287 * if an error occurs 288 */ 289 public static <IMAGE extends Image<?, IMAGE>> FlickrImageDataset<IMAGE> create(InputStreamObjectReader<IMAGE> reader, 290 FlickrAPIToken token, 291 URL url) throws Exception 292 { 293 return create(reader, token, url, 0); 294 } 295 296 /** 297 * Create an image dataset by searching flickr with the given search terms. 298 * 299 * @param reader 300 * the reader with which to load the images 301 * @param token 302 * the flickr api authentication token 303 * @param searchTerms 304 * the search terms; space separated. Prepending a term with a 305 * "-" means that the term should not appear. 306 * @return a {@link FlickrImageDataset} created from the given url 307 * @throws Exception 308 * if an error occurs 309 */ 310 public static <IMAGE extends Image<?, IMAGE>> FlickrImageDataset<IMAGE> create(InputStreamObjectReader<IMAGE> reader, 311 FlickrAPIToken token, String searchTerms) throws Exception 312 { 313 return create(reader, token, searchTerms, 0); 314 } 315 316 /** 317 * Create an image dataset by searching flickr with the given search terms. 318 * The number of images can be limited to a subset. 319 * 320 * @param reader 321 * the reader with which to load the images 322 * @param token 323 * the flickr api authentication token 324 * @param searchTerms 325 * the search terms; space separated. Prepending a term with a 326 * "-" means that the term should not appear. 327 * @param number 328 * the maximum number of images to add to the dataset. Setting to 329 * 0 or less will attempt to use all the images. 330 * @return a {@link FlickrImageDataset} created from the given url 331 * @throws Exception 332 * if an error occurs 333 */ 334 public static <IMAGE extends Image<?, IMAGE>> FlickrImageDataset<IMAGE> create(InputStreamObjectReader<IMAGE> reader, 335 FlickrAPIToken token, 336 String searchTerms, int number) throws Exception 337 { 338 final com.flickr4java.flickr.photos.SearchParameters params = new com.flickr4java.flickr.photos.SearchParameters(); 339 params.setText(searchTerms); 340 341 return createFromSearch(reader, token, params, number); 342 } 343 344 /** 345 * Create an image dataset from the flickr gallery, photoset or collection 346 * at the given url. The number of images can be limited to a subset. 347 * 348 * @param reader 349 * the reader with which to load the images 350 * @param token 351 * the flickr api authentication token 352 * @param url 353 * the url of the collection/gallery/photo-set 354 * @param number 355 * the maximum number of images to add to the dataset. Setting to 356 * 0 or less will attempt to use all the images. 357 * @return a {@link FlickrImageDataset} created from the given url 358 * @throws Exception 359 * if an error occurs 360 */ 361 public static <IMAGE extends Image<?, IMAGE>> FlickrImageDataset<IMAGE> create(InputStreamObjectReader<IMAGE> reader, 362 FlickrAPIToken token, 363 URL url, int number) throws Exception 364 { 365 final String urlString = url.toString(); 366 367 if (GALLERY_URL_PATTERN.matcher(urlString).matches()) { 368 return fromGallery(reader, token, urlString, number); 369 } else if (PHOTOSET_URL_PATTERN.matcher(urlString).matches()) { 370 return fromPhotoset(reader, token, urlString, number); 371 } else if (COLLECTION_URL_PATTERN.matcher(urlString).matches()) { 372 return fromCollection(reader, token, urlString, number); 373 } 374 375 throw new IllegalArgumentException("Unknown URL type " + urlString); 376 } 377 378 private static <IMAGE extends Image<?, IMAGE>> FlickrImageDataset<IMAGE> fromGallery( 379 InputStreamObjectReader<IMAGE> reader, 380 FlickrAPIToken token, 381 String urlString, int number) throws Exception 382 { 383 final Flickr flickr = makeFlickr(token); 384 385 final Gallery gallery = flickr.getUrlsInterface().lookupGallery(urlString); 386 387 return createFromGallery(reader, token, gallery, number); 388 } 389 390 private static <IMAGE extends Image<?, IMAGE>> FlickrImageDataset<IMAGE> fromPhotoset( 391 InputStreamObjectReader<IMAGE> reader, 392 FlickrAPIToken token, 393 String urlString, int number) throws Exception 394 { 395 final Matcher matcher = PHOTOSET_URL_PATTERN.matcher(urlString); 396 matcher.find(); 397 final String setId = matcher.group(1); 398 399 return createFromPhotoset(reader, token, setId, number); 400 } 401 402 private static <IMAGE extends Image<?, IMAGE>> FlickrImageDataset<IMAGE> fromCollection( 403 InputStreamObjectReader<IMAGE> reader, 404 FlickrAPIToken token, 405 String urlString, int number) throws Exception 406 { 407 final Matcher matcher = COLLECTION_URL_PATTERN.matcher(urlString); 408 matcher.find(); 409 final String userId = matcher.group(1); 410 final String collectionsId = matcher.group(2); 411 412 return createFromCollection(reader, token, collectionsId, userId, number); 413 } 414 415 /** 416 * Create an image dataset from a flickr gallery with the specified 417 * parameters. 418 * 419 * @param reader 420 * the reader with which to load the images 421 * @param token 422 * the flickr api authentication token 423 * @param gallery 424 * the gallery. 425 * @return a {@link FlickrImageDataset} created from the gallery described 426 * by the given parameters 427 * @throws Exception 428 * if an error occurs 429 */ 430 public static <IMAGE extends Image<?, IMAGE>> FlickrImageDataset<IMAGE> createFromGallery( 431 InputStreamObjectReader<IMAGE> reader, 432 FlickrAPIToken token, 433 Gallery gallery) throws Exception 434 { 435 return createFromGallery(reader, token, gallery.getId(), 0); 436 } 437 438 /** 439 * Create an image dataset from a flickr gallery with the specified 440 * parameters. 441 * 442 * @param reader 443 * the reader with which to load the images 444 * @param token 445 * the flickr api authentication token 446 * @param gallery 447 * the gallery. 448 * @param number 449 * the maximum number of images to add to the dataset. Setting to 450 * 0 or less will attempt to use all the images. 451 * @return a {@link FlickrImageDataset} created from the gallery described 452 * by the given parameters 453 * @throws Exception 454 * if an error occurs 455 */ 456 public static <IMAGE extends Image<?, IMAGE>> FlickrImageDataset<IMAGE> createFromGallery( 457 InputStreamObjectReader<IMAGE> reader, 458 FlickrAPIToken token, 459 Gallery gallery, 460 int number) throws Exception 461 { 462 return createFromGallery(reader, token, gallery.getId(), number); 463 } 464 465 /** 466 * Create an image dataset from a flickr gallery with the specified 467 * parameters. 468 * 469 * @param reader 470 * the reader with which to load the images 471 * @param token 472 * the flickr api authentication token 473 * @param galleryId 474 * the Flickr gallery ID. 475 * @return a {@link FlickrImageDataset} created from the gallery described 476 * by the given parameters 477 * @throws Exception 478 * if an error occurs 479 */ 480 public static <IMAGE extends Image<?, IMAGE>> FlickrImageDataset<IMAGE> createFromGallery( 481 InputStreamObjectReader<IMAGE> reader, 482 FlickrAPIToken token, 483 String galleryId) throws Exception 484 { 485 return createFromGallery(reader, token, galleryId, 0); 486 } 487 488 /** 489 * Create an image dataset from a flickr gallery with the specified 490 * parameters. The number of images can be limited to a subset. 491 * 492 * @param reader 493 * the reader with which to load the images 494 * @param token 495 * the flickr api authentication token 496 * @param galleryId 497 * the Flickr gallery ID 498 * @param number 499 * the maximum number of images to add to the dataset. Setting to 500 * 0 or less will attempt to use all the images. 501 * @return a {@link FlickrImageDataset} created from the gallery described 502 * by the given parameters 503 * @throws Exception 504 * if an error occurs 505 */ 506 public static <IMAGE extends Image<?, IMAGE>> FlickrImageDataset<IMAGE> createFromGallery( 507 InputStreamObjectReader<IMAGE> reader, 508 FlickrAPIToken token, 509 String galleryId, int number) throws Exception 510 { 511 final Flickr flickr = makeFlickr(token); 512 513 List<Photo> photos = new ArrayList<Photo>(); 514 final PhotoList<Photo> first = flickr.getGalleriesInterface().getPhotos(galleryId, Extras.ALL_EXTRAS, 250, 0); 515 photos.addAll(first); 516 517 if (number > 0) 518 number = Math.min(number, first.getTotal()); 519 520 for (int page = 1, n = photos.size(); n < number; page++) { 521 final PhotoList<Photo> result = flickr.getGalleriesInterface().getPhotos(galleryId, Extras.ALL_EXTRAS, 250, 522 page); 523 photos.addAll(result); 524 n += result.size(); 525 } 526 527 if (number > 0 && number < photos.size()) 528 photos = photos.subList(0, number); 529 530 return new FlickrImageDataset<IMAGE>(reader, photos); 531 } 532 533 /** 534 * Create an image dataset from a flickr photoset. 535 * 536 * @param reader 537 * the reader with which to load the images 538 * @param token 539 * the flickr api authentication token 540 * @param setId 541 * the photoset identifier 542 * @return a {@link FlickrImageDataset} created from the gallery described 543 * by the given parameters 544 * @throws Exception 545 * if an error occurs 546 */ 547 public static <IMAGE extends Image<?, IMAGE>> FlickrImageDataset<IMAGE> createFromPhotoset( 548 InputStreamObjectReader<IMAGE> reader, FlickrAPIToken token, String setId) throws Exception 549 { 550 return createFromPhotoset(reader, token, setId, 0); 551 } 552 553 /** 554 * Create an image dataset from a flickr photoset. The number of images can 555 * be limited to a subset. 556 * 557 * @param reader 558 * the reader with which to load the images 559 * @param token 560 * the flickr api authentication token 561 * @param setId 562 * the photoset identifier 563 * @param number 564 * the maximum number of images to add to the dataset. Setting to 565 * 0 or less will attempt to use all the images. 566 * @return a {@link FlickrImageDataset} created from the gallery described 567 * by the given parameters 568 * @throws Exception 569 * if an error occurs 570 */ 571 public static <IMAGE extends Image<?, IMAGE>> FlickrImageDataset<IMAGE> createFromPhotoset( 572 InputStreamObjectReader<IMAGE> reader, 573 FlickrAPIToken token, 574 String setId, int number) throws Exception 575 { 576 final Flickr flickr = makeFlickr(token); 577 578 final PhotosetsInterface setsInterface = flickr.getPhotosetsInterface(); 579 580 List<Photo> photos = new ArrayList<Photo>(); 581 final PhotoList<Photo> first = setsInterface.getPhotos(setId, Extras.ALL_EXTRAS, 0, 250, 0); 582 photos.addAll(first); 583 584 if (number > 0) 585 number = Math.min(number, first.getTotal()); 586 587 for (int page = 1, n = photos.size(); n < number; page++) { 588 final PhotoList<Photo> result = setsInterface.getPhotos(setId, Extras.ALL_EXTRAS, 0, 250, page); 589 photos.addAll(result); 590 n += result.size(); 591 } 592 593 if (number > 0 && number < photos.size()) 594 photos = photos.subList(0, number); 595 596 return new FlickrImageDataset<IMAGE>(reader, photos); 597 } 598 599 /** 600 * Create an image dataset from a flickr collection with the specified 601 * parameters. 602 * 603 * @param reader 604 * the reader with which to load the images 605 * @param token 606 * the flickr api authentication token 607 * @param collectionsId 608 * the collections ID 609 * @param userId 610 * the user ID 611 * @return a {@link FlickrImageDataset} created from the gallery described 612 * by the given parameters 613 * @throws Exception 614 * if an error occurs 615 */ 616 public static <IMAGE extends Image<?, IMAGE>> FlickrImageDataset<IMAGE> createFromCollection( 617 InputStreamObjectReader<IMAGE> reader, 618 FlickrAPIToken token, 619 String collectionsId, String userId) throws Exception 620 { 621 return createFromCollection(reader, token, collectionsId, userId, 0); 622 } 623 624 /** 625 * Create an image dataset from a flickr collection with the specified 626 * parameters. The number of images can be limited to a subset. 627 * 628 * @param reader 629 * the reader with which to load the images 630 * @param token 631 * the flickr api authentication token 632 * @param collectionId 633 * the collection id 634 * @param userId 635 * the user id 636 * @param number 637 * the maximum number of images to add to the dataset. Setting to 638 * 0 or less will attempt to use all the images. 639 * @return a {@link FlickrImageDataset} created from the gallery described 640 * by the given parameters 641 * @throws Exception 642 * if an error occurs 643 */ 644 public static <IMAGE extends Image<?, IMAGE>> FlickrImageDataset<IMAGE> createFromCollection( 645 InputStreamObjectReader<IMAGE> reader, 646 FlickrAPIToken token, 647 String collectionId, String userId, int number) throws Exception 648 { 649 final Flickr flickr = makeFlickr(token); 650 651 List<Photo> photos = new ArrayList<Photo>(); 652 final CollectionsInterface collectionsInterface = flickr.getCollectionsInterface(); 653 654 final List<Collection> collections = collectionsInterface.getTree(collectionId, userId); 655 for (final Collection collection : collections) 656 photos.addAll(collection.getPhotos()); 657 658 if (number > 0 && number < photos.size()) 659 photos = photos.subList(0, number); 660 661 return new FlickrImageDataset<IMAGE>(reader, photos); 662 } 663 664 /** 665 * Create an image dataset from a flickr search with the specified 666 * parameters. 667 * 668 * @param reader 669 * the reader with which to load the images 670 * @param token 671 * the flickr api authentication token 672 * @param params 673 * the parameters describing the gallery and any additional 674 * constraints. 675 * @return a {@link FlickrImageDataset} created from the gallery described 676 * by the given parameters 677 * @throws Exception 678 * if an error occurs 679 */ 680 public static <IMAGE extends Image<?, IMAGE>> FlickrImageDataset<IMAGE> createFromSearch( 681 InputStreamObjectReader<IMAGE> reader, 682 FlickrAPIToken token, 683 com.flickr4java.flickr.photos.SearchParameters params) throws Exception 684 { 685 return createFromSearch(reader, token, params, 0); 686 } 687 688 /** 689 * Create an image dataset from a flickr search with the specified 690 * parameters. The number of images can be limited to a subset. 691 * 692 * @param reader 693 * the reader with which to load the images 694 * @param token 695 * the flickr api authentication token 696 * @param params 697 * the parameters describing the gallery and any additional 698 * constraints. 699 * @param number 700 * the maximum number of images to add to the dataset. Setting to 701 * 0 or less will attempt to use all the images. 702 * @return a {@link FlickrImageDataset} created from the gallery described 703 * by the given parameters 704 * @throws Exception 705 * if an error occurs 706 */ 707 public static <IMAGE extends Image<?, IMAGE>> FlickrImageDataset<IMAGE> createFromSearch( 708 InputStreamObjectReader<IMAGE> reader, 709 FlickrAPIToken token, 710 com.flickr4java.flickr.photos.SearchParameters params, int number) throws Exception 711 { 712 final Flickr flickr = makeFlickr(token); 713 714 params.setExtras(Extras.ALL_EXTRAS); 715 716 List<Photo> photos = new ArrayList<Photo>(); 717 final PhotoList<Photo> first = flickr.getPhotosInterface().search(params, 250, 0); 718 photos.addAll(first); 719 720 if (number > 0) 721 number = Math.min(number, first.getTotal()); 722 723 for (int page = 1, n = photos.size(); n < number; page++) { 724 final PhotoList<Photo> result = flickr.getPhotosInterface().search(params, 250, page); 725 photos.addAll(result); 726 n += result.size(); 727 } 728 729 if (number > 0 && number < photos.size()) 730 photos = photos.subList(0, number); 731 732 return new FlickrImageDataset<IMAGE>(reader, photos); 733 } 734 735 private static Flickr makeFlickr(FlickrAPIToken token) throws ParserConfigurationException { 736 return new Flickr(token.apikey, token.secret, new REST()); 737 } 738}